Files
OSIT-AE-App-Svelte/src/routes/journals/ae_comp__journal_entry_obj_li.svelte
Scott Idem 8fd11d7224 feat(journals): implement Quick Add and unified Append/Prepend shared component
- Created AeCompJournalEntryQuickAdd for high-velocity note creation
- Extracted robust append/prepend logic from List View into AeCompModalJournalEntryAppend
- Unified List and Detail views to use the shared modal for content manipulation
- Added explicit Append/Prepend actions to Journal Entry settings menu
- Updated TODO.md and Journals module documentation
2026-01-13 22:59:08 -05:00

564 lines
29 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
log_lvl?: number;
lq__journal_obj: any;
lq__journal_entry_obj_li: any;
}
let { log_lvl = $bindable(0), lq__journal_obj, lq__journal_entry_obj_li }: Props = $props();
// *** Import Svelte specific
import { goto } from '$app/navigation';
import {
CalendarClock,
Check,
CodeXml,
Copy,
Eye,
EyeOff,
Files,
Fingerprint,
Flag,
Group,
ListPlus,
Lock,
NotebookPen,
NotebookText,
NotepadTextDashed,
RemoveFormatting,
Shapes,
Siren,
Tags,
TypeOutline,
X
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct
} from '$lib/stores/ae_stores';
import {
journals_sess,
journals_slct,
journals_trig,
journals_loc
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
let tmp_entry_obj: key_val = $state({});
// Derived state for modal visibility
// We cast to boolean for the prop, but we need to handle the close event to clear the store ID
let show_append_modal = $state(false);
$effect(() => {
// Sync local boolean with store ID presence
show_append_modal = !!$journals_sess.show__modal_append__journal_entry_id;
});
function handle_modal_close() {
$journals_sess.show__modal_append__journal_entry_id = null;
show_append_modal = false;
}
function handle_modal_update() {
handle_modal_close();
}
</script>
<section class="journal_list flex flex-col gap-1 md:gap-2 items-center justify-center w-full">
{#if $lq__journal_entry_obj_li && $lq__journal_entry_obj_li.length}
{#each $lq__journal_entry_obj_li as journals_journal_entry_obj, index}
<div
class="
container journal journal_entry_obj
border
px-2 py-1 space-y-1
w-full max-w-(--breakpoint-lg)
flex flex-col items-center justify-center
bg-white text-gray-900
dark:bg-gray-800 dark:text-gray-200
rounded-lg
hover:bg-gray-100 hover:dark:bg-gray-700
hover:border-gray-300
transition-all duration-500 ease-out
"
class:dim={!journals_journal_entry_obj.enable}
class:bg-warning-100={!journals_journal_entry_obj?.enable}
>
<header class="ae_header flex flex-row gap-2 items-center justify-between w-full">
<span class="flex flex-row flex-wrap gap-1">
<span class="journal_entry__name *:hover:inline-block">
{#if journals_journal_entry_obj.alert}
<Siren size="1.25em" class="mx-1 inline-block text-red-500" />
{/if}
{#if journals_journal_entry_obj.priority}
<Flag size="1.25em" class="mx-1 inline-block text-yellow-500" />
{/if}
{#if journals_journal_entry_obj.group}
<Group size="1.25em" class="mx-1 inline-block text-green-500" />
<span class="text-xs text-gray-500 hidden">Group:</span>
<span class="font-semibold text-sm text-gray-500 hidden md:inline">
{journals_journal_entry_obj.group}
</span>
{/if}
</span>
<h3 class:dim={journals_journal_entry_obj.hide} class="journal__name h4">
{#if journals_journal_entry_obj.template}
<NotepadTextDashed
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{@html journals_journal_entry_obj.name ?? '-- no name --'}
{:else if journals_journal_entry_obj.name}
<NotebookText
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{@html journals_journal_entry_obj.name}
{:else}
<CalendarClock
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.created_on,
'datetime_iso_12_no_seconds'
)}
{/if}
</h3>
<span class="flex flex-row flex-wrap gap-1 items-center justify-center">
{#if !journals_journal_entry_obj.private}
<!-- Button to copy the Markdown version -->
<button
type="button"
onclick={() => {
let tmp_entry_obj = journals_journal_entry_obj;
navigator.clipboard
.writeText(tmp_entry_obj.content)
.then(() => {
alert('Markdown content copied to clipboard!');
})
.catch((error) => {
console.error('Failed to copy content:', error);
alert('Failed to copy content.');
});
}}
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_plain_md}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
title="Copy the markdown content"
>
<RemoveFormatting size="1.25em" />
<span class="hidden"> Copy Plaintext Markdown </span>
</button>
<!-- Button to copy the rendered to HTML version -->
<button
type="button"
onclick={() => {
let htmlContent =
journals_journal_entry_obj.content_md_html || '';
navigator.clipboard
.writeText(htmlContent)
.then(() => {
alert('Rendered HTML content copied to clipboard!');
})
.catch((error) => {
console.error(
'Failed to copy HTML content:',
error
);
alert('Failed to copy HTML content.');
});
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json?.hide_copy_html}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Copy the rendered HTML content"
>
<CodeXml size="1.25em" />
<span class="hidden"> Copy HTML Markup </span>
</button>
<!-- Button to copy the rich text (rendered HTML) version -->
<button
type="button"
onclick={async () => {
const element = document.getElementById(
`rendered_journal_entry_content_${journals_journal_entry_obj.journal_entry_id}`
);
if (!element) {
console.error(
'Element not found: rendered_journal_entry_content'
);
alert('Failed to copy rich content.');
return;
}
try {
const htmlContent = element.innerHTML;
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([htmlContent], {
type: 'text/html'
})
})
]);
alert('Rendered rich content copied to clipboard!');
} catch (error) {
console.error('Failed to copy rich content:', error);
alert('Failed to copy rich content.');
}
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json?.hide_copy_rich}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Copy the rich text (rendered HTML) content"
>
<TypeOutline size="1.25em" />
<span class="hidden">Copy Rich Text</span>
</button>
<!-- Clone entry -->
<button
type="button"
onclick={() => {
let data_kv = {
code: journals_journal_entry_obj.code,
category_code: journals_journal_entry_obj.category_code,
name: journals_journal_entry_obj.name,
short_name: journals_journal_entry_obj.short_name,
content: journals_journal_entry_obj.content,
description: journals_journal_entry_obj.description,
tags: journals_journal_entry_obj.tags
};
journals_func
.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id: journals_journal_entry_obj.journal_id,
data_kv: data_kv,
log_lvl: log_lvl
})
.then((result) => {
alert('Journal entry cloned successfully!');
goto(
`/journals/${result.journal_id_random}/entry/${result.journal_entry_id_random}`
);
})
.catch((error) => {
console.error(
'Error cloning journal entry:',
error
);
alert('Failed to clone journal entry.');
});
}}
class:hidden={!journals_journal_entry_obj.template}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Clone this journal entry"
>
<Copy size="1.25em" />
<span class="hidden md:inline">Clone</span>
</button>
{:else}
<Lock
size="1.25em"
class="mx-1 inline-block text-red-400 dark:text-red-600"
/>
<span class="text-xs text-gray-500 hidden">Private</span>
<!-- Button to copy the Markdown version -->
<button
type="button"
onclick={() => {
let tmp_entry_obj = journals_journal_entry_obj;
navigator.clipboard
.writeText(tmp_entry_obj.content_encrypted)
.then(() => {
alert('Encrypted content copied to clipboard!');
})
.catch((error) => {
console.error('Failed to copy content:', error);
alert('Failed to copy content.');
});
}}
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_encrypted}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
title="Copy the encrypted content"
>
<Fingerprint size="1.25em" />
<span class="hidden"> Copy Encrypted </span>
</button>
{/if}
</span>
</span>
<div class="flex flex-row flex-wrap gap-2 items-center justify-end">
<!-- Linked file count -->
<div
class="ae_linked_file_count flex flex-row flex-wrap gap-0.5 items-center justify-start"
class:hidden={!journals_journal_entry_obj?.data_json?.hosted_file_kv}
>
<Files class="mx-1 inline-block" />
<span class="text-xs text-gray-500 hidden md:inline">Linked files:</span>
<span class="font-semibold text-sm text-gray-500">
{journals_journal_entry_obj?.data_json?.hosted_file_kv
? Object.keys(
journals_journal_entry_obj?.data_json?.hosted_file_kv
).length
: 0}×
</span>
</div>
<!-- Tags for journal entry. Comma delimited list. -->
{#if journals_journal_entry_obj.tags && journals_journal_entry_obj.tags.length}
<div
class="tags flex flex-row flex-wrap gap-0.5 items-center justify-start p-1"
>
<Tags class="mx-1 inline-block" />
<span class="text-xs text-gray-500 hidden md:inline">Tags:</span>
{#each journals_journal_entry_obj.tags.split(',') as tag}
<span
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-tertiary border border-tertiary-500 transition py-1 px-2"
title={`Tag: ${tag.trim()}`}
>
{tag.trim()}
</span>
{/each}
</div>
{/if}
<!-- Category code for journal entry -->
{#if journals_journal_entry_obj.category_code}
<button
type="button"
onclick={() => {
if (
$journals_loc.filter__category_code ==
journals_journal_entry_obj.category_code
) {
$journals_loc.filter__category_code = null;
} else {
$journals_loc.filter__category_code =
journals_journal_entry_obj.category_code;
$journals_loc.qry__category_code =
journals_journal_entry_obj.category_code;
}
$journals_trig.journal_entry_li = true;
}}
class:bg-green-100={$journals_loc.filter__category_code ==
journals_journal_entry_obj.category_code}
class="btn btn-sm variant-outline-secondary hover:preset-filled-secondary-500 transition py-1 px-2"
title={`Filter by category: ${journals_journal_entry_obj.category_code}`}
>
<Shapes class="mx-1 inline-block" />
{journals_journal_entry_obj.category_code ?? '-- no category --'}
</button>
{/if}
<a
href="/journals/{journals_journal_entry_obj?.journal_id ??
$lq__journal_obj?.journal_id}/entry/{journals_journal_entry_obj?.journal_entry_id}"
class="btn preset-tonal-primary border border-primary-500 hover:preset-filled-primary-500 transition"
title={`View ID: ${journals_journal_entry_obj?.id}\n${journals_journal_entry_obj?.name ?? ae_util.iso_datetime_formatter(journals_journal_entry_obj.created_on, 'datetime_iso_12_no_seconds')}\nJournal ID: ${journals_journal_entry_obj?.journal_id}\n`}
>
<NotebookPen class="mx-1 inline-block" />
<span class="hidden md:inline"> View </span>
</a>
<!-- Button to show a modal that will allow for a quick append to Journal Entry option. -->
<button
type="button"
onclick={() => {
$journals_sess.show__modal_append__journal_entry_id =
journals_journal_entry_obj?.id;
tmp_entry_obj = JSON.parse(
JSON.stringify(journals_journal_entry_obj)
);
}}
class="btn btn-icon btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-secondary-500 transition"
title={$lq__journal_obj?.cfg_json?.entry_add_text == 'append'
? 'Append to Journal Entry'
: 'Prepend to Journal Entry'}
>
<ListPlus />
</button>
</div>
</header>
{#if journals_journal_entry_obj.content}
<div
class:hidden={journals_journal_entry_obj.hide ||
(journals_journal_entry_obj.private &&
$journals_slct.journal_obj?.cfg_json.hide_private) ||
(journals_journal_entry_obj.personal &&
$journals_slct.journal_obj?.cfg_json.hide_personal) ||
(journals_journal_entry_obj.professional &&
$journals_slct.journal_obj?.cfg_json.hide_professional)}
class="journal__content
w-full p-1
bg-slate-100 text-gray-900
dark:bg-slate-900 dark:text-gray-100
shadow-lg rounded-lg
border border-gray-200 dark:border-gray-700
text-wrap text-sm font-mono whitespace-pre-wrap
transition-all
delay-1000 hover:delay-1000 active:delay-100
duration-1000 hover:duration-200 active:duration-200
ease-in-out
active:z-10
hover:bg-blue-100 hover:border-blue-500 dark:hover:border-blue-500
overflow-scroll
{$journals_slct.journal_obj.cfg_json.entry_li_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_max_height}`
: ''}
{$journals_slct.journal_obj.cfg_json.entry_li_click_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_click_max_height}`
: ''}
{$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height}`
: ''}
"
>
{@html journals_journal_entry_obj.content}
</div>
<article
class="prose hidden"
id="rendered_journal_entry_content_{journals_journal_entry_obj.journal_entry_id}"
>
{@html journals_journal_entry_obj?.content_md_html}
</article>
{/if}
<section
class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone}
class="ae_section journal_entry__entry"
>
<div
class="ae_group"
class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone}
>
<span class="ae_label text-sm">Original date/time:</span>
{#if journals_journal_entry_obj.original_datetime}
<span class="ae_value ae_prop prop_original_datetime font-semibold"
>{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.original_datetime,
'datetime_12_long'
)}</span
>
{/if}
{#if journals_journal_entry_obj.original_timezone}
<span class="ae_label text-sm">Timezone:</span>
<span class="ae_value font-semibold"
>{journals_journal_entry_obj.original_timezone}</span
>
{/if}
</div>
</section>
<section
class="ae_meta mt-2 flex flex-col sm:flex-row gap-2 items-center justify-center text-xs text-gray-500"
>
<span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode}
class="flex flex-row gap-1 items-center justify-center"
>
<span class="journal_entry__created_on">
Created:
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.created_on,
'datetime_12_long'
)}
</span>
<span
class="journal_entry__updated_on"
class:hidden={!journals_journal_entry_obj.updated_on}
>
Last update:
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.updated_on,
'datetime_12_long'
)}
</span>
</span>
<!-- Set/unset hide (boolean) -->
<button
type="button"
onclick={() => {
let data_kv = {
hide: journals_journal_entry_obj?.hide ? false : true
};
journals_func
.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: journals_journal_entry_obj.journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
})
.then(() => {
})
.catch((error) => {
console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.');
});
}}
class:hidden={!$ae_loc.edit_mode}
class="btn btn-sm preset-tonal-surface hover:preset-filled-warning-500 transition py-1 px-2"
title={`Set entry as ${journals_journal_entry_obj.hide ? 'visible' : 'hidden'}`}
>
{#if journals_journal_entry_obj.hide}
<EyeOff
strokeWidth="1"
color="hsla( 0, 100%, 50%, .5)"
class="inline-block"
/>
<span class="hidden md:inline">Hidden</span>
{:else}
<Eye
strokeWidth="2.5"
color="hsla( 120, 100%, 25%, .5)"
class="inline-block"
/>
<span class="hidden lg:inline">Visible</span>
{/if}
</button>
</section>
</div>
{/each}
<!-- Modal for quick append to Journal Entry -->
{#if $journals_sess.show__modal_append__journal_entry_id}
<AeCompModalJournalEntryAppend
bind:open={show_append_modal}
journal_entry={tmp_entry_obj}
journal_config={$lq__journal_obj?.cfg_json}
onClose={handle_modal_close}
onUpdate={handle_modal_update}
{log_lvl}
/>
{/if}
{:else}
<p>No journal entry available to show.</p>
{/if}
</section>