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
This commit is contained in:
Scott Idem
2026-01-13 22:59:08 -05:00
parent 80bc5e453a
commit 8fd11d7224
9 changed files with 403 additions and 398 deletions

View File

@@ -29,8 +29,8 @@ This is a list of tasks to be completed before the next event/show/conference.
- [ ] **Error Transparency**: Update backend to return specific SQLAlchemy/Pydantic errors in `meta.details`.
- [ ] **Automated Source of Truth**: Generate `V3_OBJECT_MODELS.md` automatically in `agents_sync/Aether/`.
- [ ] **Phase 2: UI/UX Excellence**
- [ ] Implement "Quick Add" for high-velocity entry.
- [ ] Add rapid append/prepend functionality to existing entries.
- [x] Implement "Quick Add" for high-velocity entry.
- [x] Add rapid append/prepend functionality to existing entries.
- [ ] Ensure full cross-platform responsiveness (Mobile -> Workstation).
- [ ] **Phase 3: Content Portability**
- [ ] Implement Structured Markdown import (with metadata).

View File

@@ -73,13 +73,13 @@ This document outlines the modernization of the Journals module UI in the Svelte
- [x] Verify frontend uses V3 API (`ae_journals__journal.ts`).
### Phase 2: Rapid Entry (Active)
- [ ] Create `ae_comp__journal_entry_quick_add.svelte`.
- [ ] Integrate Quick Add into `+page.svelte`.
- [x] Create `ae_comp__journal_entry_quick_add.svelte`.
- [x] Integrate Quick Add into `+page.svelte`.
- [ ] Update `ae_journals_stores.ts` to manage Quick Add state/visibility.
### Phase 3: Content Manipulation
- [ ] Update `JournalEntry_SettingsMenu.svelte` with Append/Prepend actions.
- [ ] Implement Append/Prepend logic in `JournalEntry_Editor.svelte` or helper functions.
- [x] Update `JournalEntry_SettingsMenu.svelte` with Append/Prepend actions.
- [x] Implement Append/Prepend logic in `ae_comp__journal_entry_obj_id_view.svelte`.
### Phase 4: Polish & Security
- [ ] Audit encryption flow for Quick Added entries.

View File

@@ -32,6 +32,7 @@
import Modal_journals_cfg from './modal_journals_config.svelte';
import Journal_obj_li from './ae_comp__journal_obj_li.svelte';
import AeCompJournalEntryQuickAdd from './ae_comp__journal_entry_quick_add.svelte';
// import Element_data_store from '$lib/element_data_store_v2.svelte';
@@ -175,6 +176,11 @@
{$ae_loc.person.given_name ? `- ${$ae_loc.person.given_name}` : ''}
</h1>
<!-- Quick Add Section -->
<div class="w-full max-w-2xl mx-auto px-4 pb-4">
<AeCompJournalEntryQuickAdd class="shadow-lg" />
</div>
<div
class="flex flex-row flex-wrap gap-1 items-center justify-center w-full border-gray-200 border-y-2 py-2"
class:hidden={!$ae_loc.edit_mode}

View File

@@ -26,6 +26,8 @@
onDecrypt: () => void;
onDecryptHistory: () => void;
onChangeJournal: () => void;
onAppend?: () => void;
onPrepend?: () => void;
log_lvl?: number;
}
@@ -39,6 +41,8 @@
onDecrypt,
onDecryptHistory,
onChangeJournal,
onAppend,
onPrepend,
log_lvl = 0
}: Props = $props();
@@ -220,6 +224,8 @@ p-2 md:p-3 rounded-lg shadow-md
{onSave}
{onDecryptHistory}
{onChangeJournal}
{onAppend}
{onPrepend}
{log_lvl}
/>
</div>

View File

@@ -9,7 +9,8 @@
Eye, EyeOff, ShieldCheck, ShieldMinus,
Clock, X, Trash2, Settings, Shapes,
Copy, RemoveFormatting, CodeXml, TypeOutline,
History, Pencil, PenLine, FileX, SquareLibrary
History, Pencil, PenLine, FileX, SquareLibrary,
ArrowUpToLine, ArrowDownToLine
} from '@lucide/svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { journals_slct, journals_loc, journals_sess } from '$lib/ae_journals/ae_journals_stores';
@@ -26,6 +27,8 @@
onSave: () => void;
onDecryptHistory: () => void;
onChangeJournal: () => void;
onAppend?: () => void;
onPrepend?: () => void;
log_lvl?: number;
}
@@ -37,6 +40,8 @@
onSave,
onDecryptHistory,
onChangeJournal,
onAppend,
onPrepend,
log_lvl = 0
}: Props = $props();
@@ -83,6 +88,16 @@
</script>
<div class="space-y-4 min-w-[280px]">
<!-- Append/Prepend Actions -->
<div class="grid grid-cols-2 gap-2">
<button class="btn btn-sm variant-soft-secondary" onclick={onPrepend} title="Prepend to Note">
<ArrowUpToLine size="1.2em" class="mr-1"/> Prepend
</button>
<button class="btn btn-sm variant-soft-secondary" onclick={onAppend} title="Append to Note">
<ArrowDownToLine size="1.2em" class="mr-1"/> Append
</button>
</div>
<!-- Category selection -->
<div class="flex items-center gap-2">
<Shapes size="1.1em" class="text-surface-500" />

View File

@@ -31,6 +31,7 @@
import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte';
import JournalEntry_Editor from './JournalEntry_Editor.svelte';
import JournalEntry_Header from './JournalEntry_Header.svelte';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
// *** Props
interface Props {
@@ -486,6 +487,22 @@
slct_hosted_file_obj = null;
}
});
// --- Shared Append / Prepend Modal Logic ---
let show_append_modal = $state(false);
let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
function handle_append_start() {
modal_mode = 'append';
show_append_modal = true;
$journals_sess.entry.show_menu = false;
}
function handle_prepend_start() {
modal_mode = 'prepend';
show_append_modal = true;
$journals_sess.entry.show_menu = false;
}
</script>
<section
@@ -508,6 +525,8 @@
onDecrypt={handle_content_decryption}
onDecryptHistory={handle_history_decryption}
onChangeJournal={change_journal_id}
onAppend={handle_append_start}
onPrepend={handle_prepend_start}
{log_lvl}
/>
@@ -596,7 +615,18 @@
<AE_MetadataFooter obj={tmp_entry_obj} />
<AeCompModalJournalEntryAppend
bind:open={show_append_modal}
journal_entry={$lq__journal_entry_obj}
journal_config={$lq__journal_obj?.cfg_json}
mode={modal_mode}
onClose={() => (show_append_modal = false)}
onUpdate={() => {
show_append_modal = false;
updated_obj = true; // Force refresh of the view
}}
{log_lvl}
/>
{:else}
<section class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-center w-full">
<span class="text-lg text-orange-900 dark:text-orange-100">

View File

@@ -8,8 +8,7 @@
let { log_lvl = $bindable(0), lq__journal_obj, lq__journal_entry_obj_li }: Props = $props();
// *** Import Svelte specific
import { goto, invalidate, pushState, replaceState } from '$app/navigation';
import { Modal } from 'flowbite-svelte';
import { goto } from '$app/navigation';
import {
CalendarClock,
Check,
@@ -17,11 +16,9 @@
Copy,
Eye,
EyeOff,
FileLock,
Files,
Fingerprint,
Flag,
FlagOff,
Group,
ListPlus,
Lock,
@@ -39,50 +36,45 @@
// *** 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 { api } from '$lib/api/api';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
slct
} from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_trig
journals_trig,
journals_loc
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
let ae_promises: key_val = $state({});
// let ae_tmp: key_val = {};
// let ae_triggers: key_val = {};
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
let tmp_entry_obj: key_val = $state({});
let tmp_entry_obj_add_timestamp_header: boolean = $state(true);
let tmp_entry_obj_add_timestamp_header_w_day_of_week: boolean = $state(true);
let tmp_entry_obj_add_text_header: string = $state('');
let tmp_entry_obj_add_text: string = $state('');
let tmp_entry_obj_changed: boolean = $state(false);
// 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(() => {
if (tmp_entry_obj_add_text_header.length || tmp_entry_obj_add_text) {
tmp_entry_obj_changed = true;
} else {
tmp_entry_obj_changed = false;
}
// 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}
<!-- <div class="ae_group">
<span class="ae_label">Group:</span>
<span class="ae_value">{current_group}</span> -->
{#each $lq__journal_entry_obj_li as journals_journal_entry_obj, index}
<div
class="
@@ -163,11 +155,9 @@
});
}}
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_plain_md}
class="btn btn-sm p-1 preset-tonal-secondary hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
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"
>
<!-- <span class="fas fa-copy mx-1"></span> -->
<Copy size="1em" />
<RemoveFormatting size="1.25em" />
<span class="hidden"> Copy Plaintext Markdown </span>
</button>
@@ -176,8 +166,6 @@
<button
type="button"
onclick={() => {
// Copy the rendered HTML content to clipboard
// const htmlContent = $lq__journal_entry_obj?.content_md_html || '';
let htmlContent =
journals_journal_entry_obj.content_md_html || '';
@@ -196,11 +184,9 @@
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json?.hide_copy_html}
class="btn btn-sm p-1 preset-tonal-secondary hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
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"
>
<!-- <span class="fas fa-copy mx-1"></span> -->
<Copy size="1em" />
<CodeXml size="1.25em" />
<span class="hidden"> Copy HTML Markup </span>
</button>
@@ -221,10 +207,7 @@
}
try {
// Get the rendered HTML content
const htmlContent = element.innerHTML;
// Use the Clipboard API to write the HTML content as rich text
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([htmlContent], {
@@ -241,10 +224,9 @@
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json?.hide_copy_rich}
class="btn btn-sm p-1 preset-tonal-secondary *:hover:inline lg:text-xs"
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"
>
<Copy size="1em" />
<TypeOutline size="1.25em" />
<span class="hidden">Copy Rich Text</span>
</button>
@@ -253,8 +235,6 @@
<button
type="button"
onclick={() => {
// Clone the journal entry
// We only want to clone certain fields from the original journal entry object to avoid conflicts.
let data_kv = {
code: journals_journal_entry_obj.code,
category_code: journals_journal_entry_obj.category_code,
@@ -287,23 +267,17 @@
});
}}
class:hidden={!journals_journal_entry_obj.template}
class="btn btn-sm p-1 preset-tonal-secondary hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Clone this journal entry"
>
<!-- class="btn btn-sm variant-soft-surface hover:variant-filled-warning transition py-1 px-2" -->
<!-- <Copy strokeWidth="1" /> -->
<Copy size="1.25em" />
<span class="hidden md:inline">Clone</span>
</button>
<!-- </span> -->
{:else}
<!-- <span class="flex flex-row flex-wrap gap-1 items-center justify-center"> -->
<Lock
size="1.25em"
class="mx-1 inline-block text-red-400 dark:text-red-600"
/>
<!-- <EyeOff 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 -->
@@ -323,16 +297,12 @@
});
}}
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_encrypted}
class="btn btn-sm p-1 preset-tonal-secondary hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
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"
>
<!-- <span class="fas fa-copy mx-1"></span> -->
<Copy size="1em" />
<Fingerprint size="1.25em" />
<span class="hidden"> Copy Encrypted </span>
</button>
<!-- </span> -->
{/if}
</span>
</span>
@@ -344,8 +314,7 @@
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="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(
@@ -376,11 +345,9 @@
<!-- Category code for journal entry -->
{#if journals_journal_entry_obj.category_code}
<!-- When clicked, this will filter by the category code. -->
<button
type="button"
onclick={() => {
// WARNING: This will cause pages to reset if the journal entry list is being filtered by category. This is a bug that should be fixed.
if (
$journals_loc.filter__category_code ==
journals_journal_entry_obj.category_code
@@ -389,17 +356,10 @@
} else {
$journals_loc.filter__category_code =
journals_journal_entry_obj.category_code;
// We also want to set the category code used when creating a new journal entry.
$journals_loc.qry__category_code =
journals_journal_entry_obj.category_code;
}
$journals_trig.journal_entry_li = true;
if (log_lvl) {
console.log(
'$journals_loc.filter__category_code',
$journals_loc.filter__category_code
);
}
}}
class:bg-green-100={$journals_loc.filter__category_code ==
journals_journal_entry_obj.category_code}
@@ -427,7 +387,6 @@
onclick={() => {
$journals_sess.show__modal_append__journal_entry_id =
journals_journal_entry_obj?.id;
// Create a deep copy of the object for editing to prevent direct mutation of the liveQuery result.
tmp_entry_obj = JSON.parse(
JSON.stringify(journals_journal_entry_obj)
);
@@ -438,13 +397,11 @@
: 'Prepend to Journal Entry'}
>
<ListPlus />
<!-- Append -->
</button>
</div>
</header>
{#if journals_journal_entry_obj.content}
<!-- hover:border-gray-500 dark:hover:border-gray-500 -->
<div
class:hidden={journals_journal_entry_obj.hide ||
(journals_journal_entry_obj.private &&
@@ -483,7 +440,6 @@
{@html journals_journal_entry_obj.content}
</div>
<!-- This needs to remain hidden for the copy function to work and us not seeing the rendered HTML version. -->
<article
class="prose hidden"
id="rendered_journal_entry_content_{journals_journal_entry_obj.journal_entry_id}"
@@ -497,28 +453,6 @@
!journals_journal_entry_obj?.original_timezone}
class="ae_section journal_entry__entry"
>
<!-- {#if journals_journal_entry_obj?.description}
<div
class="journal_entry__description ae_description"
>
<div class="ae_label journal_entry__description text-sm">Description:</div>
<div class="ae_value journal_entry__description">
{@html journals_journal_entry_obj?.description}
</div>
</div>
{/if} -->
<!-- {#if journals_journal_entry_obj?.entry_html}
<div
class="journal_entry__entry_html ae_entry_html"
>
<div class="ae_label journal_entry__entry_html text-sm">Content:</div>
<div class="ae_value journal_entry__entry_html">
{@html journals_journal_entry_obj?.entry_html}
</div>
</div>
{/if} -->
<div
class="ae_group"
class:hidden={!journals_journal_entry_obj?.original_datetime &&
@@ -545,12 +479,6 @@
<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="journal_entry__journal_entry_type"
class:hidden={!journals_journal_entry_obj.journal_entry_type}
>
Type: {journals_journal_entry_obj.journal_entry_type}
</span> -->
<span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode}
class="flex flex-row gap-1 items-center justify-center"
@@ -589,8 +517,6 @@
log_lvl: log_lvl
})
.then(() => {
// Optionally, you can provide feedback to the user
// alert('Journal entry updated successfully!');
})
.catch((error) => {
console.error('Error updating journal entry:', error);
@@ -622,298 +548,17 @@
{/each}
<!-- Modal for quick append to Journal Entry -->
<!-- This should only have a textarea, Save button, and Cancel button. -->
{#if $journals_sess.show__modal_append__journal_entry_id}
<Modal
title="{$lq__journal_obj?.cfg_json?.entry_add_text == 'append'
? 'Append to Journal Entry'
: 'Prepend to Journal Entry'}: {tmp_entry_obj?.name ??
tmp_entry_obj?.created_on} ({tmp_entry_obj?.journal_entry_id})"
bind:open={$journals_sess.show__modal_append__journal_entry_id}
autoclose={false}
placement="top-center"
size="xl"
class="
top-center
bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200
rounded-lg border-gray-200 dark:border-gray-700
divide-gray-200 dark:divide-gray-700
shadow-md relative
flex flex-col gap-1 mx-auto w-full"
>
<div class="modal">
<div class="modal-box">
<!-- <h3 class="font-bold text-lg">Edit Journal</h3> -->
<div class="flex flex-col gap-1">
<!-- Checkbox for using the timestamp as the Markdown header. This will be pre-pended to any text header given, if any. -->
<div>
<input
type="checkbox"
id="append_timestamp_header"
bind:checked={tmp_entry_obj_add_timestamp_header}
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
inline-block
"
/>
<label
for="append_timestamp_header"
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
inline-block
"
>
Use timestamp as Markdown header
</label>
<input
type="checkbox"
id="append_timestamp_header_w_day_of_week"
bind:checked={tmp_entry_obj_add_timestamp_header_w_day_of_week}
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
inline-block
"
/>
<label
for="append_timestamp_header_w_day_of_week"
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
inline-block
"
>
Include day of week
</label>
</div>
<input
type="text"
placeholder="Markdown header for appended content"
bind:value={tmp_entry_obj_add_text_header}
onchange={() => {
// tmp_entry_obj_changed = true;
}}
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
grow min-h-12 h-full w-full
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
"
/>
<textarea
bind:value={tmp_entry_obj_add_text}
onchange={() => {
// tmp_entry_obj_changed = true;
}}
class:border-orange-200={$ae_loc.edit_mode}
class:hover:border-orange-500={$ae_loc.edit_mode}
class="
grow min-h-48 h-full w-full
p-2
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
hover:border-gray-500 dark:hover:border-gray-500
"
placeholder="Append to journal entry content..."
></textarea>
</div>
<div class="modal-action">
<button
type="button"
disabled={!tmp_entry_obj_changed}
onclick={async () => {
let current_entry_content = tmp_entry_obj?.content;
let add_content = ''; // tmp_entry_obj?.content + '\n\n';
let new_content = current_entry_content;
console.log(
`Append or Prepend? ${$lq__journal_obj?.cfg_json?.entry_add_text}`
);
console.log(tmp_entry_obj_add_text_header);
console.log(tmp_entry_obj_add_text);
// if ($lq__journal_obj?.cfg_json?.entry_add_text == 'prepend') {
if (
tmp_entry_obj_add_timestamp_header &&
tmp_entry_obj_add_text_header
) {
add_content =
'## ' +
ae_util.iso_datetime_formatter(
new Date(),
'datetime_iso_12_no_seconds'
) +
(tmp_entry_obj_add_timestamp_header_w_day_of_week
? ' (' +
ae_util.iso_datetime_formatter(
new Date(),
'week_long'
) +
')'
: '') +
' - ' +
tmp_entry_obj_add_text_header.trim() +
'\n' +
tmp_entry_obj_add_text.trim() +
'\n\n';
} else if (tmp_entry_obj_add_timestamp_header) {
add_content =
'## ' +
ae_util.iso_datetime_formatter(
new Date(),
'datetime_iso_12_no_seconds'
) +
(tmp_entry_obj_add_timestamp_header_w_day_of_week
? ' (' +
ae_util.iso_datetime_formatter(
new Date(),
'week_long'
) +
')'
: '') +
'\n' +
tmp_entry_obj_add_text.trim() +
'\n\n';
} else if (tmp_entry_obj_add_text_header) {
add_content =
'## ' +
tmp_entry_obj_add_text_header.trim() +
(tmp_entry_obj_add_timestamp_header_w_day_of_week
? ' (' +
ae_util.iso_datetime_formatter(
new Date(),
'week_long'
) +
')'
: '') +
'\n' +
tmp_entry_obj_add_text.trim() +
'\n\n';
}
// } else {
// if (tmp_entry_obj_add_timestamp_header && tmp_entry_obj_add_text_header) {
// new_content = new_content + '## ' + ae_util.iso_datetime_formatter(new Date(), 'datetime_iso_12_no_seconds') + ' - ' + tmp_entry_obj_add_text_header.trim() + '\n';
// } else if (tmp_entry_obj_add_timestamp_header) {
// new_content = new_content + '## ' + ae_util.iso_datetime_formatter(new Date(), 'datetime_iso_12_no_seconds') + '\n';
// } else if (tmp_entry_obj_add_text_header) {
// new_content = new_content + '## ' + tmp_entry_obj_add_text_header.trim() + '\n';
// }
// }
// if (tmp_entry_obj_add_text_header) {
// new_content = new_content + '# ' + tmp_entry_obj_add_text_header.trim() + '\n';
// }
// if (tmp_entry_obj_add_text) {
if ($lq__journal_obj?.cfg_json?.entry_add_text == 'prepend') {
// Handle prepending content
new_content = add_content + new_content;
} else {
// Handle appending content
// Add one more line break before the content to be appended
new_content = new_content + '\n' + add_content;
}
// new_content = new_content + tmp_entry_obj_add_text.trim();
// }
new_content = new_content.trim() + '\n';
let data_kv = {
content: new_content
};
let update_result = await journals_func
.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj?.journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
})
.then((result) => {
// Optionally, you can provide feedback to the user
// alert('Journal entry updated successfully!');
return result;
})
.catch((error) => {
console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.');
})
.finally(() => {});
if (update_result) {
// alert('Journal entry updated successfully!');
// Reset the form
tmp_entry_obj_add_text_header = '';
tmp_entry_obj_add_text = '';
tmp_entry_obj_changed = false;
$journals_sess.show__modal_append__journal_entry_id = false;
} else {
alert('Failed to update journal entry.');
}
}}
class:preset-filled-error-500={tmp_entry_obj_changed}
class="
btn btn-sm md:btn-md lg:btn-lg
min-w-72 w-full lg:min-w-96
max-w-96
hover:variant-outline-success
hover:preset-filled-success-500
"
>
<Check />
Update
</button>
<button
type="button"
onclick={$journals_sess.show__modal_append__journal_entry_id ??
false}
class="btn preset-tonal-surface border border-surface-500 hover:preset-filled-surface-500 transition"
>
<X />
Cancel
</button>
</div>
</div>
</div>
</Modal>
<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>
<!-- {/if} -->
</section>

View File

@@ -0,0 +1,106 @@
<script lang="ts">
import { api } from '$lib/api/api';
import { ae_api } from '$lib/stores/ae_stores';
import { journals_slct, journals_loc, journals_trig } from '$lib/ae_journals/ae_journals_stores';
// Props
let {
class: className = "",
placeholder = "Type your quick note... (First line = Title)"
} = $props();
// State
let note_content = $state("");
let is_submitting = $state(false);
// Derived
// Determine target journal: Selected > Default Preference > First Available?
let target_journal_id = $derived($journals_slct.journal_id || $journals_loc.qry__journal_id);
async function handle_submit() {
if (!note_content.trim()) return;
if (!target_journal_id) {
alert("No journal selected!");
return;
}
is_submitting = true;
const lines = note_content.trim().split('\n');
let name = lines[0].substring(0, 100);
if (lines[0].length > 100) name += "...";
// If content is just one line, name is content, content is content.
// If multiple lines, name is line 0, content is full text.
const payload = {
journal_id: target_journal_id,
name: name,
content: note_content,
type_code: 'note', // Default to note
// created_on: handled by backend usually, or we can send ISO
enabled: true,
hidden: false
};
// Use the store value directly via $ prefix
const api_cfg = $ae_api;
const res = await api.create_ae_obj_v3({
api_cfg: api_cfg,
obj_type: 'journal_entry',
fields: payload
});
if (res) {
note_content = "";
// Trigger refresh
$journals_trig.journal_entry_li = true;
} else {
alert("Failed to create note.");
}
is_submitting = false;
}
function handle_keydown(e: KeyboardEvent) {
if (e.ctrlKey && e.key === 'Enter') {
handle_submit();
}
}
</script>
<div class="card p-4 space-y-4 variant-filled-surface {className}">
<header class="flex justify-between items-center">
<h3 class="h3">Quick Add</h3>
{#if !target_journal_id}
<span class="badge variant-filled-error">No Journal Selected</span>
{/if}
</header>
<textarea
class="textarea"
rows="3"
bind:value={note_content}
{placeholder}
onkeydown={handle_keydown}
disabled={is_submitting}
></textarea>
<div class="flex justify-end space-x-2">
<button
class="btn variant-ghost-surface"
onclick={() => note_content = ""}
disabled={is_submitting || note_content.length === 0}
>
Clear
</button>
<button
class="btn variant-filled-primary"
onclick={handle_submit}
disabled={is_submitting || !target_journal_id || note_content.length === 0}
>
{#if is_submitting}Saving...{:else}Add Note{/if}
</button>
</div>
</div>

View File

@@ -0,0 +1,197 @@
<script lang="ts">
import { Modal } from 'flowbite-svelte';
import { Check, X } from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores';
interface Props {
open: boolean;
journal_entry: key_val;
journal_config: key_val; // The cfg_json from the journal object
mode?: 'append' | 'prepend' | 'auto';
onClose: () => void;
onUpdate: () => void;
log_lvl?: number;
}
let {
open = $bindable(false),
journal_entry,
journal_config,
mode = 'auto',
onClose,
onUpdate,
log_lvl = 0
}: Props = $props();
// Local State
let tmp_entry_obj: key_val = $state({});
// Header Options
let add_timestamp_header: boolean = $state(true);
let add_timestamp_header_w_day_of_week: boolean = $state(true);
let add_text_header: string = $state('');
let add_text: string = $state('');
// Change detection
let has_changes: boolean = $derived(add_text_header.length > 0 || add_text.length > 0);
// Initialize tmp object when entry changes or modal opens
$effect(() => {
if (open && journal_entry) {
tmp_entry_obj = JSON.parse(JSON.stringify(journal_entry));
// Reset fields
add_text_header = '';
add_text = '';
}
});
async function handle_save() {
let current_entry_content = tmp_entry_obj?.content || '';
let add_content = '';
let new_content = current_entry_content;
// Construct the header/content to add (Following original logic)
let timestamp_str = ae_util.iso_datetime_formatter(
new Date(),
'datetime_iso_12_no_seconds'
);
let day_of_week_str = add_timestamp_header_w_day_of_week
? ' (' + ae_util.iso_datetime_formatter(new Date(), 'week_long') + ')'
: '';
if (add_timestamp_header && add_text_header) {
add_content =
'## ' +
timestamp_str +
day_of_week_str +
' - ' +
add_text_header.trim() +
'\n' +
add_text.trim() +
'\n\n';
} else if (add_timestamp_header) {
add_content = '## ' + timestamp_str + day_of_week_str + '\n' + add_text.trim() + '\n\n';
} else if (add_text_header) {
add_content =
'## ' + add_text_header.trim() + day_of_week_str + '\n' + add_text.trim() + '\n\n';
} else {
add_content = add_text.trim() + '\n\n';
}
// Determine Append or Prepend
let effective_mode = mode;
if (effective_mode === 'auto') {
effective_mode = journal_config?.entry_add_text || 'append';
}
if (effective_mode == 'prepend') {
new_content = add_content + new_content;
} else {
// Append
new_content = new_content.trim() + '\n\n' + add_content;
}
new_content = new_content.trim() + '\n';
let data_kv = { content: new_content };
try {
let update_result = await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj?.journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
});
if (update_result) {
// Success
onUpdate();
open = false;
} else {
alert('Failed to update journal entry.');
}
} catch (error) {
console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.');
}
}
</script>
<Modal
title="{(mode === 'auto' ? journal_config?.entry_add_text : mode) == 'append' ? 'Append to' : 'Prepend to'} Journal Entry: {journal_entry?.name ?? journal_entry?.created_on}"
bind:open={open}
autoclose={false}
placement="top-center"
size="xl"
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative flex flex-col gap-1 mx-auto w-full"
>
<div class="modal">
<div class="modal-box">
<div class="flex flex-col gap-1">
<!-- Checkbox Options -->
<div>
<input
type="checkbox"
id="append_timestamp_header"
bind:checked={add_timestamp_header}
class="p-2 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 hover:border-gray-500 dark:hover:border-gray-500 inline-block"
/>
<label for="append_timestamp_header" class="p-2 inline-block">
Use timestamp as Markdown header
</label>
<input
type="checkbox"
id="append_timestamp_header_w_day_of_week"
bind:checked={add_timestamp_header_w_day_of_week}
class="p-2 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 hover:border-gray-500 dark:hover:border-gray-500 inline-block"
/>
<label for="append_timestamp_header_w_day_of_week" class="p-2 inline-block">
Include day of week
</label>
</div>
<!-- Text Header Input -->
<input
type="text"
placeholder="Markdown header for content (Optional)"
bind:value={add_text_header}
class="grow min-h-12 h-full w-full p-2 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 hover:border-gray-500 dark:hover:border-gray-500"
/>
<!-- Main Content Area -->
<textarea
bind:value={add_text}
class="grow min-h-48 h-full w-full p-2 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 hover:border-gray-500 dark:hover:border-gray-500"
placeholder="Content to {mode === 'auto'
? journal_config?.entry_add_text
: mode}...">
</textarea>
</div>
<div class="modal-action flex justify-end gap-2 mt-4">
<button
type="button"
disabled={!has_changes}
onclick={handle_save}
class="btn btn-sm md:btn-md lg:btn-lg min-w-32 hover:variant-outline-success hover:preset-filled-success-500"
class:preset-filled-primary-500={has_changes}
class:preset-filled-surface-500={!has_changes}
>
<Check class="mr-1" />
Update
</button>
<button
type="button"
onclick={onClose}
class="btn preset-tonal-surface border border-surface-500 hover:preset-filled-surface-500 transition"
>
<X class="mr-1" />
Cancel
</button>
</div>
</div>
</div>
</Modal>