- 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
564 lines
29 KiB
Svelte
564 lines
29 KiB
Svelte
<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> |