Prettier for Journals

This commit is contained in:
Scott Idem
2026-03-24 12:25:22 -04:00
parent e1338b1a72
commit b74c6d0e9c
25 changed files with 3983 additions and 4238 deletions

View File

@@ -1,86 +1,95 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').LayoutProps} */ /** @type {import('./$types').LayoutProps} */
let log_lvl = $state(0); let log_lvl = $state(0);
// *** Import Svelte specific // *** Import Svelte specific
import { untrack } from 'svelte'; import { untrack } from 'svelte';
// import { browser } from '$app/environment'; // import { browser } from '$app/environment';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { ArrowDownUp, ArrowRight, House, RefreshCw, Satellite } from '@lucide/svelte'; import {
ArrowDownUp,
ArrowRight,
House,
RefreshCw,
Satellite
} from '@lucide/svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_slct, journals_slct,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import Element_data_store from '$lib/elements/element_data_store.svelte'; import Element_data_store from '$lib/elements/element_data_store.svelte';
import Help_tech from '$lib/app_components/e_app_help_tech.svelte'; import Help_tech from '$lib/app_components/e_app_help_tech.svelte';
// *** Setup Svelte properties // *** Setup Svelte properties
interface Props { interface Props {
data: any; data: any;
children: any; children: any;
} }
let { data, children }: Props = $props(); let { data, children }: Props = $props();
// Use effects for store initializations to prevent render-phase updates // Use effects for store initializations to prevent render-phase updates
$effect(() => { $effect(() => {
untrack(() => {
if ($slct.account_id !== data.account_id) {
$slct.account_id = data.account_id;
}
});
});
let ae_acct = $derived(data[data.account_id]);
$effect(() => {
if (ae_acct) {
untrack(() => { untrack(() => {
if ($slct.account_id !== data.account_id) { if ($journals_slct.journal_id !== ae_acct.slct.journal_id) {
$slct.account_id = data.account_id; $journals_slct.journal_id = ae_acct.slct.journal_id;
}
if (
JSON.stringify($journals_slct.journal_obj_li) !==
JSON.stringify(ae_acct.slct.journal_obj_li)
) {
$journals_slct.journal_obj_li = ae_acct.slct.journal_obj_li;
} }
}); });
});
let ae_acct = $derived(data[data.account_id]);
$effect(() => {
if (ae_acct) {
untrack(() => {
if ($journals_slct.journal_id !== ae_acct.slct.journal_id) {
$journals_slct.journal_id = ae_acct.slct.journal_id;
}
if (JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(ae_acct.slct.journal_obj_li)) {
$journals_slct.journal_obj_li = ae_acct.slct.journal_obj_li;
}
});
}
});
let nav_y_height = $state(0);
let box: any = $state(null);
let xLeft = $state(0);
let xScroll = $state(0);
let xWidth = $state(0);
let yTop = $state(0);
let yScroll = $state(0);
let yHeight = $state(0);
function handle_scroll() {
// console.log(`handle_scroll() called`);
if (box) {
xLeft = box.scrollLeft;
xScroll = box.scrollWidth;
xWidth = box.clientWidth;
yTop = box.scrollTop;
yHeight = box.clientHeight;
yScroll = box.scrollHeight;
// console.log(`handle_scroll() called: ${yTop}`);
}
} }
});
function scroll_container() { let nav_y_height = $state(0);
return (
document.getElementById('ae_main_content') || let box: any = $state(null);
document.documentElement || let xLeft = $state(0);
document.body let xScroll = $state(0);
); let xWidth = $state(0);
let yTop = $state(0);
let yScroll = $state(0);
let yHeight = $state(0);
function handle_scroll() {
// console.log(`handle_scroll() called`);
if (box) {
xLeft = box.scrollLeft;
xScroll = box.scrollWidth;
xWidth = box.clientWidth;
yTop = box.scrollTop;
yHeight = box.clientHeight;
yScroll = box.scrollHeight;
// console.log(`handle_scroll() called: ${yTop}`);
} }
}
function scroll_container() {
return (
document.getElementById('ae_main_content') ||
document.documentElement ||
document.body
);
}
</script> </script>
<svelte:head> <svelte:head>
@@ -97,15 +106,14 @@
class:iframe={$ae_loc?.iframe} class:iframe={$ae_loc?.iframe}
class=" class="
ae_journals ae_journals
h-full max-h-full max-w-7xl m-auto flex h-full
max-h-full
max-w-7xl flex-col gap-1
overflow-auto overflow-auto
flex flex-col gap-1
m-auto
bg-gray-50 dark:bg-gray-900 bg-gray-50 text-gray-800
text-gray-800 dark:text-gray-200 dark:bg-gray-900 dark:text-gray-200
" ">
>
<!-- class:hidden={yTop > 200} --> <!-- class:hidden={yTop > 200} -->
<nav <nav
bind:clientHeight={nav_y_height} bind:clientHeight={nav_y_height}
@@ -113,36 +121,33 @@
class:opacity-0={yTop > 250} class:opacity-0={yTop > 250}
class=" class="
submenu submenu
z-20 absolute
hover:opacity-100 top-0
absolute top-0 left-0 right-0 right-0 left-0 z-20 m-auto
w-full max-w-7xl flex min-h-12
min-h-12 w-full
p-1 px-2 pb-2 m-auto max-w-7xl flex-row flex-wrap items-center
flex flex-row flex-wrap justify-around gap-1 rounded-b-lg
items-center justify-around sm:justify-between border-b-2 bg-gray-200 p-1
gap-1 px-2
border-b-2 rounded-b-lg pb-2 transition-all
bg-gray-200 dark:bg-gray-800 duration-1000 hover:opacity-100
transition-all duration-1000 sm:justify-between dark:bg-gray-800
" ">
>
<span class="justify-self-start"> <span class="justify-self-start">
<!-- Be sure to explain what &AElig; (Aether) means in the title text or similar! --> <!-- Be sure to explain what &AElig; (Aether) means in the title text or similar! -->
<Satellite <Satellite
size="1.5em" size="1.5em"
class="mx-1 inline-block text-gray-500" class="mx-1 inline-block text-gray-500" />
/>
<abbr title="Aether - Journals Module"> Æ Journals </abbr> <abbr title="Aether - Journals Module"> Æ Journals </abbr>
</span> </span>
<a <a
href="/" href="/"
class="btn btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-success-500" class="btn btn-sm preset-tonal-surface border-surface-500 hover:preset-filled-success-500 border">
>
<House /> <House />
<span class="hidden md:inline"> Home </span> <span class="hidden md:inline"> Home </span>
</a> </a>
@@ -213,9 +218,8 @@
// window.location.reload(true); // true only works with Firefox // window.location.reload(true); // true only works with Firefox
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.'); // alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
}} }}
class="btn btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-warning-500" class="btn btn-sm preset-tonal-surface border-surface-500 hover:preset-filled-warning-500 border"
title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared." title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared.">
>
<!-- <span class="fas fa-eraser mx-1"></span> --> <!-- <span class="fas fa-eraser mx-1"></span> -->
<!-- <span class="fas fa-sync mx-1"></span> --> <!-- <span class="fas fa-sync mx-1"></span> -->
<RefreshCw /> <RefreshCw />
@@ -230,8 +234,7 @@
show_btn_class="btn-info" show_btn_class="btn-info"
additional_kv={{ additional_kv={{
test: true test: true
}} }}></Help_tech>
></Help_tech>
</nav> </nav>
<!-- Add overflow-auto to section element to have the main nav sort of sticky at top --> <!-- Add overflow-auto to section element to have the main nav sort of sticky at top -->
@@ -244,23 +247,21 @@
class=" class="
main_content main_content
grow grow
px-1 md:px-2 px-1 pb-48
pb-48 md:px-2
" ">
>
{@render children?.()} {@render children?.()}
</section> </section>
<div <div
class:hidden={yTop < 500} class:hidden={yTop < 500}
class=" class="
z-20 fixed
hover:opacity-100 right-1
fixed bottom-48 right-1 bottom-48 z-20 flex
flex flex-col gap-1 items-end justify-end flex-col items-end justify-end gap-1 hover:opacity-100
" ">
>
<!-- Scroll to top button --> <!-- Scroll to top button -->
<button <button
type="button" type="button"
@@ -293,8 +294,7 @@
window.parent.postMessage({ scroll_to: 0 }, '*'); window.parent.postMessage({ scroll_to: 0 }, '*');
}} }}
title="Scroll to top" title="Scroll to top">
>
<ArrowDownUp class="rotate-180" /> <ArrowDownUp class="rotate-180" />
Scroll to Top Scroll to Top
</button> </button>
@@ -321,8 +321,7 @@
window.parent.postMessage({ scroll_to: xScroll }, '*'); window.parent.postMessage({ scroll_to: xScroll }, '*');
}} }}
title="Scroll to right" title="Scroll to right">
>
<ArrowRight size="1em" class="inline" /> <ArrowRight size="1em" class="inline" />
<!-- Scroll to Right <!-- Scroll to Right
xLeft={xLeft} xScroll={xScroll} xWidth={xWidth} xScroll={xScroll} scrollLeft={scroll_container().scrollLeft} xLeft={xLeft} xScroll={xScroll} xWidth={xWidth} xScroll={xScroll} scrollLeft={scroll_container().scrollLeft}
@@ -360,8 +359,7 @@
window.parent.postMessage({ scroll_to: yScroll }, '*'); window.parent.postMessage({ scroll_to: yScroll }, '*');
}} }}
title="Scroll to bottom" title="Scroll to bottom">
>
<ArrowDownUp /> <ArrowDownUp />
Scroll to Bottom Scroll to Bottom
</button> </button>
@@ -373,38 +371,35 @@
class:opacity-0={yTop > 250} class:opacity-0={yTop > 250}
class=" class="
footer footer
z-20 absolute
hover:opacity-100 right-0
absolute bottom-0 left-0 right-0 bottom-0 left-0 z-20 m-auto
w-full max-w-7xl flex w-full
p-1 m-auto max-w-7xl flex-row
flex flex-row flex-wrap flex-wrap items-center justify-between
items-center justify-between gap-1 rounded-t-lg
sm:flex-row md:items-center md:justify-between border-t-2 border-gray-200 bg-gray-200
gap-1 p-1
border-t-2 border-gray-200 dark:border-gray-600 text-xs transition-all duration-1000
rounded-t-lg hover:text-base
bg-gray-200 dark:bg-gray-800 hover:opacity-100 sm:flex-row
text-xs hover:text-base md:items-center md:justify-between
transition-all duration-1000 dark:border-gray-600 dark:bg-gray-800
" "
class:ae_debug={$ae_loc?.debug} class:ae_debug={$ae_loc?.debug}>
>
<Element_data_store <Element_data_store
ds_code="hub__site__appshell_footer" ds_code="hub__site__appshell_footer"
ds_type="html" ds_type="html"
class_li="grow flex flex-row justify-between" class_li="grow flex flex-row justify-between" />
/>
</footer> </footer>
</div> </div>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You are not logged in as a user. You must be signed in to access the You are not logged in as a user. You must be signed in to access the
journals module. journals module.

View File

@@ -1,103 +1,108 @@
<script lang="ts"> <script lang="ts">
/** /**
* src/routes/journals/+page.svelte * src/routes/journals/+page.svelte
* Modernized Journals Index View * Modernized Journals Index View
* Focus: Simplicity for regular users, power tools for edit mode. * Focus: Simplicity for regular users, power tools for edit mode.
*/ */
// import { onMount } from 'svelte'; // import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Icons // *** Icons
import { BookPlus, FileUp, LoaderCircle, Sparkles, SquareLibrary, Wrench } from '@lucide/svelte'; import {
// *** Libraries & Stores BookPlus,
import { liveQuery } from 'dexie'; FileUp,
import { Modal } from 'flowbite-svelte'; LoaderCircle,
import { db_core } from '$lib/ae_core/db_core'; Sparkles,
import { db_journals } from '$lib/ae_journals/db_journals'; SquareLibrary,
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; Wrench
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores'; } from '@lucide/svelte';
import { // *** Libraries & Stores
journals_loc, import { liveQuery } from 'dexie';
journals_sess, import { Modal } from 'flowbite-svelte';
journals_slct, import { db_core } from '$lib/ae_core/db_core';
journals_trig import { db_journals } from '$lib/ae_journals/db_journals';
} from '$lib/ae_journals/ae_journals_stores'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_trig
} from '$lib/ae_journals/ae_journals_stores';
// *** Components // *** Components
import AE_Comp_Modal_Journal_Config from './ae_comp__modal_journal_config.svelte'; import AE_Comp_Modal_Journal_Config from './ae_comp__modal_journal_config.svelte';
import Journal_obj_li from './ae_comp__journal_obj_li.svelte'; import Journal_obj_li from './ae_comp__journal_obj_li.svelte';
import AE_Comp_Journal_Entry_Quick_Add from './ae_comp__journal_entry_quick_add.svelte'; import AE_Comp_Journal_Entry_Quick_Add from './ae_comp__journal_entry_quick_add.svelte';
import AE_Comp_Modal_Journal_Import from './ae_comp__modal_journal_import.svelte'; import AE_Comp_Modal_Journal_Import from './ae_comp__modal_journal_import.svelte';
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
// *** State // *** State
let show_import_modal = $state(false); let show_import_modal = $state(false);
let log_lvl = 0; let log_lvl = 0;
// *** LiveQueries // *** LiveQueries
let lq__account = $derived( let lq__account = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!$slct.account_id) return null; if (!$slct.account_id) return null;
return await db_core.account.get($slct.account_id); return await db_core.account.get($slct.account_id);
}) })
); );
let lq__journal_obj_li = $derived( let lq__journal_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
return await db_journals.journal return await db_journals.journal
.where('person_id') .where('person_id')
.equals($ae_loc.person_id) .equals($ae_loc.person_id)
.reverse() .reverse()
.sortBy('tmp_sort_3'); .sortBy('tmp_sort_3');
}) })
); );
async function create_journal() { async function create_journal() {
if (!confirm('Create a new journal?')) return; if (!confirm('Create a new journal?')) return;
const name = $journals_sess.journal.new_journal_name; const name = $journals_sess.journal.new_journal_name;
const type = $journals_sess.journal.new_journal_type_code; const type = $journals_sess.journal.new_journal_type_code;
if (name && type) { if (name && type) {
try { try {
const results = await journals_func.create_ae_obj__journal({ const results = await journals_func.create_ae_obj__journal({
api_cfg: $ae_api, api_cfg: $ae_api,
account_id: $slct.account_id, account_id: $slct.account_id,
data_kv: { data_kv: {
person_id: $ae_loc.person_id, person_id: $ae_loc.person_id,
name, name,
type_code: type, type_code: type,
cfg_json: { cfg_json: {
category_li: [{ code: '', name: 'Default' }] category_li: [{ code: '', name: 'Default' }]
}
} }
});
if (results?.journal_id) {
$journals_sess.show__modal_new__journal_obj = false;
goto(`/journals/${results.journal_id}`);
} }
} catch (error) { });
alert('Failed to create journal.'); if (results?.journal_id) {
$journals_sess.show__modal_new__journal_obj = false;
goto(`/journals/${results.journal_id}`);
} }
} else { } catch (error) {
alert('Please provide a name and type.'); alert('Failed to create journal.');
} }
} else {
alert('Please provide a name and type.');
} }
}
</script> </script>
<div <div
class="page_container flex flex-col gap-4 sm:gap-6 md:gap-8 items-center w-full min-h-screen p-3 sm:p-4 md:p-8" class="page_container flex min-h-screen w-full flex-col items-center gap-4 p-3 sm:gap-6 sm:p-4 md:gap-8 md:p-8">
>
<!-- Header Section --> <!-- Header Section -->
<header class="text-center space-y-2 max-w-3xl"> <header class="max-w-3xl space-y-2 text-center">
<h1 <h1
class="text-3xl sm:text-4xl md:text-5xl font-black tracking-tight text-surface-900 dark:text-surface-100" class="text-surface-900 dark:text-surface-100 text-3xl font-black tracking-tight sm:text-4xl md:text-5xl">
>
<SquareLibrary size="1em" class="text-primary-500 inline-block" /> <SquareLibrary size="1em" class="text-primary-500 inline-block" />
Journals Journals
</h1> </h1>
@@ -105,7 +110,9 @@
{#if $ae_loc.person.given_name || $ae_loc.person.family_name} {#if $ae_loc.person.given_name || $ae_loc.person.family_name}
<p class="text-surface-600 dark:text-surface-400 font-medium"> <p class="text-surface-600 dark:text-surface-400 font-medium">
<span class="text-primary-500 font-semibold"> <span class="text-primary-500 font-semibold">
{[$ae_loc.person.given_name, $ae_loc.person.family_name].filter(Boolean).join(' ')} {[$ae_loc.person.given_name, $ae_loc.person.family_name]
.filter(Boolean)
.join(' ')}
</span> </span>
</p> </p>
{/if} {/if}
@@ -113,19 +120,17 @@
<!-- Quick Add Integrated Section --> <!-- Quick Add Integrated Section -->
<section class="w-full max-w-2xl"> <section class="w-full max-w-2xl">
<div class="relative group"> <div class="group relative">
<!-- Glow ring: slightly brighter in dark mode where colors need more presence --> <!-- Glow ring: slightly brighter in dark mode where colors need more presence -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 dark:opacity-40 group-hover:opacity-60 dark:group-hover:opacity-70 transition duration-1000 group-hover:duration-200" class="from-primary-500 to-secondary-500 absolute -inset-1 rounded-2xl bg-linear-to-r opacity-25 blur transition duration-1000 group-hover:opacity-60 group-hover:duration-200 dark:opacity-40 dark:group-hover:opacity-70">
></div> </div>
<AE_Comp_Journal_Entry_Quick_Add <AE_Comp_Journal_Entry_Quick_Add
journals_li={$lq__journal_obj_li} journals_li={$lq__journal_obj_li}
class="relative shadow-2xl rounded-xl overflow-hidden border border-surface-500/10 bg-surface-50 dark:bg-surface-900" class="border-surface-500/10 bg-surface-50 dark:bg-surface-900 relative overflow-hidden rounded-xl border shadow-2xl" />
/>
</div> </div>
<div <div
class="mt-2 flex items-center justify-center gap-2 text-xs opacity-50 font-bold uppercase tracking-widest" class="mt-2 flex items-center justify-center gap-2 text-xs font-bold tracking-widest uppercase opacity-50">
>
<Sparkles size="1em" /> <Sparkles size="1em" />
<span>Fast Input Mode Active</span> <span>Fast Input Mode Active</span>
</div> </div>
@@ -134,33 +139,29 @@
<!-- Administrative Action Bar (Edit Mode Only) --> <!-- Administrative Action Bar (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<nav <nav
class="flex flex-row flex-wrap gap-3 items-center justify-center w-full py-4 border-y border-surface-500/10" class="border-surface-500/10 flex w-full flex-row flex-wrap items-center justify-center gap-3 border-y py-4">
>
<button <button
type="button" type="button"
class="btn preset-tonal-secondary shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-secondary shadow-lg transition-transform hover:scale-105"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = true)} ($journals_sess.show__modal_new__journal_obj = true)}>
>
<BookPlus size="1.2em" class="mr-2" /> <BookPlus size="1.2em" class="mr-2" />
<span>New Journal</span> <span>New Journal</span>
</button> </button>
<button <button
type="button" type="button"
class="btn preset-tonal-surface shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-surface shadow-lg transition-transform hover:scale-105"
onclick={() => (show_import_modal = true)} onclick={() => (show_import_modal = true)}>
>
<FileUp size="1.2em" class="mr-2" /> <FileUp size="1.2em" class="mr-2" />
<span>Import</span> <span>Import</span>
</button> </button>
<button <button
type="button" type="button"
class="btn preset-tonal-surface shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-surface shadow-lg transition-transform hover:scale-105"
onclick={() => onclick={() =>
($journals_sess.show__modal__journals_config = true)} ($journals_sess.show__modal__journals_config = true)}>
>
<Wrench size="1.2em" class="mr-2" /> <Wrench size="1.2em" class="mr-2" />
<span>Config</span> <span>Config</span>
</button> </button>
@@ -168,11 +169,10 @@
{/if} {/if}
<!-- Main List Section --> <!-- Main List Section -->
<main class="w-full flex justify-center"> <main class="flex w-full justify-center">
{#if $lq__journal_obj_li === undefined} {#if $lq__journal_obj_li === undefined}
<div <div
class="flex flex-col items-center justify-center p-20 gap-4 opacity-50" class="flex flex-col items-center justify-center gap-4 p-20 opacity-50">
>
<LoaderCircle size="3em" class="animate-spin" /> <LoaderCircle size="3em" class="animate-spin" />
<p class="text-xl font-bold">Accessing Brain...</p> <p class="text-xl font-bold">Accessing Brain...</p>
</div> </div>
@@ -180,11 +180,10 @@
<Journal_obj_li {lq__journal_obj_li} /> <Journal_obj_li {lq__journal_obj_li} />
{:else} {:else}
<div <div
class="max-w-md text-center p-12 bg-surface-500/5 rounded-3xl border-2 border-dashed border-surface-500/20" class="bg-surface-500/5 border-surface-500/20 max-w-md rounded-3xl border-2 border-dashed p-12 text-center">
>
<SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" /> <SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" />
<h3 class="text-2xl font-bold mb-2">No Journals Found</h3> <h3 class="mb-2 text-2xl font-bold">No Journals Found</h3>
<p class="opacity-60 mb-6"> <p class="mb-6 opacity-60">
You haven't created any journals yet. Start by creating one You haven't created any journals yet. Start by creating one
to begin your documentation journey. to begin your documentation journey.
</p> </p>
@@ -192,8 +191,7 @@
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = true)} ($journals_sess.show__modal_new__journal_obj = true)}>
>
Create Your First Journal Create Your First Journal
</button> </button>
</div> </div>
@@ -208,32 +206,27 @@
bind:open={$journals_sess.show__modal_new__journal_obj} bind:open={$journals_sess.show__modal_new__journal_obj}
autoclose={false} autoclose={false}
size="md" size="md"
class="bg-white dark:bg-surface-900 shadow-2xl rounded-2xl" class="dark:bg-surface-900 rounded-2xl bg-white shadow-2xl">
> <div class="space-y-4 p-2">
<div class="p-2 space-y-4">
<div class="space-y-1"> <div class="space-y-1">
<!-- svelte-ignore a11y_label_has_associated_control --> <!-- svelte-ignore a11y_label_has_associated_control -->
<label class="label text-sm font-bold opacity-75" <label class="label text-sm font-bold opacity-75"
>Journal Name</label >Journal Name</label>
>
<input <input
type="text" type="text"
placeholder="e.g. My Daily Logs" placeholder="e.g. My Daily Logs"
bind:value={$journals_sess.journal.new_journal_name} bind:value={$journals_sess.journal.new_journal_name}
class="input" class="input" />
/>
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<!-- svelte-ignore a11y_label_has_associated_control --> <!-- svelte-ignore a11y_label_has_associated_control -->
<label class="label text-sm font-bold opacity-75" <label class="label text-sm font-bold opacity-75"
>Type Code</label >Type Code</label>
>
<input <input
type="text" type="text"
placeholder="e.g. diary, log, notebook" placeholder="e.g. diary, log, notebook"
bind:value={$journals_sess.journal.new_journal_type_code} bind:value={$journals_sess.journal.new_journal_type_code}
class="input" class="input" />
/>
</div> </div>
<div class="flex justify-end gap-2 pt-4"> <div class="flex justify-end gap-2 pt-4">
<button <button
@@ -241,13 +234,11 @@
class="btn preset-tonal-surface" class="btn preset-tonal-surface"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = false)} ($journals_sess.show__modal_new__journal_obj = false)}
>Cancel</button >Cancel</button>
>
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold" class="btn preset-filled-primary font-bold"
onclick={create_journal}>Create Journal</button onclick={create_journal}>Create Journal</button>
>
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -255,12 +246,10 @@
{#if $journals_sess.show__modal__journals_config} {#if $journals_sess.show__modal__journals_config}
<AE_Comp_Modal_Journal_Config <AE_Comp_Modal_Journal_Config
show={$journals_sess.show__modal__journals_config} show={$journals_sess.show__modal__journals_config} />
/>
{/if} {/if}
<AE_Comp_Modal_Journal_Import <AE_Comp_Modal_Journal_Import
bind:open={show_import_modal} bind:open={show_import_modal}
on_close={() => (show_import_modal = false)} on_close={() => (show_import_modal = false)}
on_import_complete={() => {}} on_import_complete={() => {}} />
/>

View File

@@ -1,83 +1,83 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').LayoutProps} */ /** @type {import('./$types').LayoutProps} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
let { data, children } = $props(); let { data, children } = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { FilePlus, Notebook, SquareLibrary, X } from '@lucide/svelte'; import { FilePlus, Notebook, SquareLibrary, X } from '@lucide/svelte';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct journals_slct
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import type { ae_JournalEntry } from '$lib/types/ae_types'; import type { ae_JournalEntry } from '$lib/types/ae_types';
import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte'; import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store) // NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration. // to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log(`ae_acct = `, ae_acct); console.log(`ae_acct = `, ae_acct);
} }
}); });
let show_menu__all_journals: boolean = $state(false); let show_menu__all_journals: boolean = $state(false);
let lq__journal_obj = $derived( let lq__journal_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal.get( let results = await db_journals.journal.get(
$journals_slct?.journal_id ?? '' $journals_slct?.journal_id ?? ''
); // null or undefined does not reset things like '' does ); // null or undefined does not reset things like '' does
// Check if results are different than the current session version stored under $journals_slct // Check if results are different than the current session version stored under $journals_slct
if ($journals_slct.journal_obj && results) { if ($journals_slct.journal_obj && results) {
if ( if (
JSON.stringify($journals_slct.journal_obj) !== JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(results) JSON.stringify(results)
) { ) {
$journals_slct.journal_obj = { ...results }; $journals_slct.journal_obj = { ...results };
}
}
return results;
})
);
$effect(() => {
if (log_lvl) {
console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`
);
console.log(`lq__journal_obj: results = `, lq__journal_obj);
if ($journals_slct.journal_obj && lq__journal_obj) {
if (
JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(lq__journal_obj)
) {
console.log(
`Session slct stored version has changed for ID = ${$journals_slct.journal_id}`,
$journals_slct.journal_obj
);
} else {
console.log(
`Session slct stored version has not changed for ID = ${$journals_slct.journal_id}`
);
}
} }
} }
});
return results;
})
);
$effect(() => {
if (log_lvl) {
console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`
);
console.log(`lq__journal_obj: results = `, lq__journal_obj);
if ($journals_slct.journal_obj && lq__journal_obj) {
if (
JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(lq__journal_obj)
) {
console.log(
`Session slct stored version has changed for ID = ${$journals_slct.journal_id}`,
$journals_slct.journal_obj
);
} else {
console.log(
`Session slct stored version has not changed for ID = ${$journals_slct.journal_id}`
);
}
}
}
});
</script> </script>
<!-- Svelte layout for a Journal ID page and children --> <!-- Svelte layout for a Journal ID page and children -->
@@ -85,29 +85,27 @@
class=" class="
ae_journals__journal ae_journals__journal
mx-auto mx-auto
flex flex-col grow gap-1 flex max-h-max min-h-full max-w-max
items-center
min-h-full
max-h-max
min-w-full min-w-full
max-w-max grow
flex-col
items-center
gap-1
space-y-2 space-y-2
" ">
>
<div <div
class=" class="
flex flex-row flex-wrap relative flex w-full
flex-row
flex-wrap items-center
justify-between
gap-1 gap-1
items-center justify-between
border-gray-400
border-b border-b
py-2 border-gray-400
w-full py-2 transition-all
hover:bg-slate-100 hover:dark:bg-slate-700
relative transition-all hover:bg-slate-100 hover:dark:bg-slate-700
" ">
>
<!-- If middle click then open the all journals page in a new tab. Otherwise show/hide the menu. --> <!-- If middle click then open the all journals page in a new tab. Otherwise show/hide the menu. -->
<button <button
type="button" type="button"
@@ -134,8 +132,7 @@
transition-all transition-all
" "
title={`View all journals menu: "${$ae_loc?.user?.name}" title={`View all journals menu: "${$ae_loc?.user?.name}"
Middle-click to open in new tab`} Middle-click to open in new tab`}>
>
<!-- <BookHeart /> --> <!-- <BookHeart /> -->
<!-- <Library /> --> <!-- <Library /> -->
{#if show_menu__all_journals} {#if show_menu__all_journals}
@@ -152,16 +149,15 @@ Middle-click to open in new tab`}
<div <div
class=" class="
absolute top-12 left-0 absolute top-12 left-0
p-4 z-50 w-80 z-50 w-80 max-w-fit
space-y-0.5
bg-white dark:bg-gray-800
border border-gray-500
shadow-xl rounded-lg
min-w-72 min-w-72
max-w-fit space-y-0.5 rounded-lg
border border-gray-500
bg-white p-4
shadow-xl
dark:bg-gray-800
" "
class:hidden={!show_menu__all_journals} class:hidden={!show_menu__all_journals}>
>
<a <a
href="/journals" href="/journals"
class=" class="
@@ -172,8 +168,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="View all journals for this account: {$ae_loc.account_name}" title="View all journals for this account: {$ae_loc.account_name}">
>
<!-- <BookHeart /> --> <!-- <BookHeart /> -->
<!-- <Library /> --> <!-- <Library /> -->
<SquareLibrary class="text-blue-500" /> <SquareLibrary class="text-blue-500" />
@@ -195,12 +190,11 @@ Middle-click to open in new tab`}
}} }}
class=" class="
form-select form-select
border-neutral-400-600
w-full w-full
border p-1
text-sm text-sm
border border-neutral-400-600 ">
p-1
"
>
<option value="" disabled selected> <option value="" disabled selected>
{Object.keys($journals_loc.entry_view_history_kv) {Object.keys($journals_loc.entry_view_history_kv)
.length}&times; Recent Entries... .length}&times; Recent Entries...
@@ -228,8 +222,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="View all journal entries for this journal: {$lq__journal_obj?.name}" title="View all journal entries for this journal: {$lq__journal_obj?.name}">
>
<Notebook /> <Notebook />
<!-- <Bookmark /> --> <!-- <Bookmark /> -->
<!-- <BookHeart class="m-1" /> --> <!-- <BookHeart class="m-1" /> -->
@@ -315,8 +308,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="Create a new journal entry for this journal: {$lq__journal_obj?.name}" title="Create a new journal entry for this journal: {$lq__journal_obj?.name}">
>
<FilePlus /> <FilePlus />
<!-- <span class="fas fa-plus m-1"></span> --> <!-- <span class="fas fa-plus m-1"></span> -->
<span class="hidden sm:inline"> New Entry </span> <span class="hidden sm:inline"> New Entry </span>

View File

@@ -1,295 +1,291 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').PageData} */ /** @type {import('./$types').PageData} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
// *** Import other supporting libraries // *** Import other supporting libraries
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_prom, journals_prom,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompJournalObjIdView from './../ae_comp__journal_obj_id_view.svelte'; import AeCompJournalObjIdView from './../ae_comp__journal_obj_id_view.svelte';
import Journal_entry_obj_li_wrapper from './../ae_comp__journal_entry_obj_li_wrapper.svelte'; import Journal_entry_obj_li_wrapper from './../ae_comp__journal_entry_obj_li_wrapper.svelte';
import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte'; import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte';
import AeCompModalJournalImport from '../ae_comp__modal_journal_import.svelte'; import AeCompModalJournalImport from '../ae_comp__modal_journal_import.svelte';
// Variables // Variables
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
let show_export_modal = $state(false); let show_export_modal = $state(false);
let show_import_modal = $state(false); let show_import_modal = $state(false);
let search_id_li: Array<string> = $state([]); let search_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null; let search_debounce_timer: any = null;
let last_search_id = 0; let last_search_id = 0;
let last_executed_key = ''; // Search Guard Key let last_executed_key = ''; // Search Guard Key
function handle_import_complete() { function handle_import_complete() {
// Trigger a refresh of the journal entry list // Trigger a refresh of the journal entry list
if ($journals_loc.entry.search_version === undefined) if ($journals_loc.entry.search_version === undefined)
$journals_loc.entry.search_version = 0; $journals_loc.entry.search_version = 0;
$journals_loc.entry.search_version++; $journals_loc.entry.search_version++;
} }
$effect(() => { $effect(() => {
if (!ae_acct) return; if (!ae_acct) return;
untrack(() => {
$journals_slct.journal_id = ae_acct.slct.journal_id;
$journals_slct.journal_entry_id = null;
});
});
let lq__journal_obj = $derived(
liveQuery(async () => {
return await db_journals.journal.get($journals_slct?.journal_id ?? '');
})
);
// Stable LiveQuery Pattern (Aether UI V3)
// Re-wrapped in $derived to ensure the observable instance remains stable
// unless the underlying dependencies (ids, search context) change.
// Important: keep the `liveQuery` closure free of transient reactive
// references — capture stable values (ids, search keys) so the observable
// isn't recreated unnecessarily on every render. Use `search_id_li` or
// other plain arrays/values as explicit dependencies.
let lq__journal_entry_obj_li = $derived(
liveQuery(async () => {
const ids = search_id_li;
const journal_id = $lq__journal_obj?.journal_id;
const search_text = $journals_loc.entry.qry__search_text;
const cat_code = $journals_loc.entry.qry__category_code;
// SCENARIO 1: Specific IDs provided (Search Results)
if (Array.isArray(ids) && ids.length > 0) {
if (log_lvl)
console.log(`Journal Page LQ: bulkGet ${ids.length} IDs`);
const results = await db_journals.journal_entry.bulkGet(ids);
return results.filter((item) => item !== undefined);
}
// SCENARIO 2: Fallback to broad search (Default view)
if (journal_id && !search_text && !cat_code) {
if (log_lvl)
console.log(
`Journal Page LQ: Fallback search for journal: ${journal_id}`
);
return await db_journals.journal_entry
.where('journal_id')
.equals(journal_id)
.reverse()
.sortBy('tmp_sort_1');
}
return [];
})
);
// Standardized Reactive Search Pattern (Aether UI V3)
// 1. Isolate dependencies into a stable derived object
let search_params = $derived({
v: $journals_loc.entry.search_version,
str: ($journals_loc.entry.qry__search_text ?? '').toLowerCase().trim(),
cat: $journals_loc.entry.qry__category_code,
limit: $journals_loc.entry.qry__limit,
enabled: $journals_loc.entry.qry__enabled,
hidden: $journals_loc.entry.qry__hidden,
journal_id: $journals_slct.journal_id,
person_id: $ae_loc.person_id,
remote_first: $journals_loc.entry.qry__remote_first
});
// 2. Controlled effect for triggering searches
$effect(() => {
// Establishes reactive dependency on search_params
const params = search_params;
if (search_debounce_timer) clearTimeout(search_debounce_timer);
search_debounce_timer = setTimeout(() => {
// Execution MUST be untracked to prevent circular triggers
untrack(() => { untrack(() => {
$journals_slct.journal_id = ae_acct.slct.journal_id; handle_search_refresh(params);
$journals_slct.journal_entry_id = null;
}); });
}, 250);
return () => {
if (search_debounce_timer) clearTimeout(search_debounce_timer);
};
});
async function handle_search_refresh(params: any) {
// 1. Guard: Check if criteria actually changed
const qry_key = JSON.stringify(params);
if (qry_key === last_executed_key) return;
last_executed_key = qry_key;
const current_search_id = ++last_search_id;
const journal_id = params.journal_id;
const remote_first = params.remote_first;
if (log_lvl)
console.log(
`[Journal Search #${current_search_id}] Refreshing entries (remote=${remote_first}, journal=${journal_id})...`
);
// 2. Setup State
untrack(() => {
$journals_sess.entry.qry__status = 'loading';
}); });
let lq__journal_obj = $derived( const qry_str = params.str;
liveQuery(async () => { const cat_code = params.cat;
return await db_journals.journal.get(
$journals_slct?.journal_id ?? ''
);
})
);
// Stable LiveQuery Pattern (Aether UI V3) let local_ids: string[] = [];
// Re-wrapped in $derived to ensure the observable instance remains stable
// unless the underlying dependencies (ids, search context) change.
// Important: keep the `liveQuery` closure free of transient reactive
// references — capture stable values (ids, search keys) so the observable
// isn't recreated unnecessarily on every render. Use `search_id_li` or
// other plain arrays/values as explicit dependencies.
let lq__journal_entry_obj_li = $derived(
liveQuery(async () => {
const ids = search_id_li;
const journal_id = $lq__journal_obj?.journal_id;
const search_text = $journals_loc.entry.qry__search_text;
const cat_code = $journals_loc.entry.qry__category_code;
// SCENARIO 1: Specific IDs provided (Search Results) // 3. FAST PATH: Local IDB Search (SWR)
if (Array.isArray(ids) && ids.length > 0) { // We skip this ONLY if remote_first is checked AND we have search text
if (log_lvl) if (!remote_first) {
console.log(`Journal Page LQ: bulkGet ${ids.length} IDs`); try {
const results = await db_journals.journal_entry.bulkGet(ids); if (journal_id) {
return results.filter((item) => item !== undefined); let local_results = await db_journals.journal_entry
}
// SCENARIO 2: Fallback to broad search (Default view)
if (journal_id && !search_text && !cat_code) {
if (log_lvl)
console.log(
`Journal Page LQ: Fallback search for journal: ${journal_id}`
);
return await db_journals.journal_entry
.where('journal_id') .where('journal_id')
.equals(journal_id) .equals(journal_id)
.reverse() .filter((entry) => {
.sortBy('tmp_sort_1'); if (cat_code && entry.category_code !== cat_code)
} return false;
if (qry_str) {
return []; const name = (entry.name ?? '').toLowerCase();
}) const content = (entry.content ?? '').toLowerCase();
); return (
name.includes(qry_str) ||
// Standardized Reactive Search Pattern (Aether UI V3) content.includes(qry_str)
// 1. Isolate dependencies into a stable derived object
let search_params = $derived({
v: $journals_loc.entry.search_version,
str: ($journals_loc.entry.qry__search_text ?? '').toLowerCase().trim(),
cat: $journals_loc.entry.qry__category_code,
limit: $journals_loc.entry.qry__limit,
enabled: $journals_loc.entry.qry__enabled,
hidden: $journals_loc.entry.qry__hidden,
journal_id: $journals_slct.journal_id,
person_id: $ae_loc.person_id,
remote_first: $journals_loc.entry.qry__remote_first
});
// 2. Controlled effect for triggering searches
$effect(() => {
// Establishes reactive dependency on search_params
const params = search_params;
if (search_debounce_timer) clearTimeout(search_debounce_timer);
search_debounce_timer = setTimeout(() => {
// Execution MUST be untracked to prevent circular triggers
untrack(() => {
handle_search_refresh(params);
});
}, 250);
return () => {
if (search_debounce_timer) clearTimeout(search_debounce_timer);
};
});
async function handle_search_refresh(params: any) {
// 1. Guard: Check if criteria actually changed
const qry_key = JSON.stringify(params);
if (qry_key === last_executed_key) return;
last_executed_key = qry_key;
const current_search_id = ++last_search_id;
const journal_id = params.journal_id;
const remote_first = params.remote_first;
if (log_lvl)
console.log(
`[Journal Search #${current_search_id}] Refreshing entries (remote=${remote_first}, journal=${journal_id})...`
);
// 2. Setup State
untrack(() => {
$journals_sess.entry.qry__status = 'loading';
});
const qry_str = params.str;
const cat_code = params.cat;
let local_ids: string[] = [];
// 3. FAST PATH: Local IDB Search (SWR)
// We skip this ONLY if remote_first is checked AND we have search text
if (!remote_first) {
try {
if (journal_id) {
let local_results = await db_journals.journal_entry
.where('journal_id')
.equals(journal_id)
.filter((entry) => {
if (cat_code && entry.category_code !== cat_code)
return false;
if (qry_str) {
const name = (entry.name ?? '').toLowerCase();
const content = (
entry.content ?? ''
).toLowerCase();
return (
name.includes(qry_str) ||
content.includes(qry_str)
);
}
return true;
})
.toArray();
local_results.sort((a, b) => {
const dateA = a.updated_on
? new Date(a.updated_on).getTime()
: 0;
const dateB = b.updated_on
? new Date(b.updated_on).getTime()
: 0;
return dateB - dateA;
});
local_ids = local_results
.map((e) => e.id || e.journal_entry_id)
.filter(Boolean);
if (current_search_id === last_search_id) {
if (log_lvl)
console.log(
`[Journal Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`
); );
untrack(() => { }
search_id_li = local_ids; return true;
if (local_ids.length > 0) })
$journals_sess.entry.qry__status = 'done'; .toArray();
});
}
}
} catch (e) {
if (log_lvl) console.warn('Journal Fast Path failed.', e);
}
}
// 4. REVALIDATE: API Request local_results.sort((a, b) => {
try { const dateA = a.updated_on
const results = await journals_func.qry__journal_entry({ ? new Date(a.updated_on).getTime()
api_cfg: $ae_api, : 0;
journal_id: journal_id, const dateB = b.updated_on
person_id: null, ? new Date(b.updated_on).getTime()
qry_str: qry_str || null, : 0;
qry_category_code: cat_code || null, return dateB - dateA;
enabled: params.enabled, });
hidden: params.hidden,
limit: params.limit,
log_lvl: 0
});
if (current_search_id === last_search_id) { local_ids = local_results
const api_results = results || []; .map((e) => e.id || e.journal_entry_id)
const api_ids = api_results
.map((e: any) => e.id || e.journal_entry_id)
.filter(Boolean); .filter(Boolean);
// Protect UI cache if API returns empty during revalidation if (current_search_id === last_search_id) {
if ( if (log_lvl)
api_ids.length === 0 && console.log(
local_ids.length > 0 && `[Journal Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`
!remote_first && );
!qry_str
) {
untrack(() => { untrack(() => {
$journals_sess.entry.qry__status = 'done';
});
return;
}
untrack(() => {
$journals_sess.entry_li = api_results;
search_id_li = api_ids;
$journals_sess.entry.qry__status = 'done';
});
if (log_lvl)
console.log(
`[Journal Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`
);
}
} catch (error) {
if (current_search_id === last_search_id) {
console.error('Journal revalidation failed:', error);
untrack(() => {
$journals_sess.entry.qry__status = 'error';
if (search_id_li.length === 0 && local_ids.length > 0) {
search_id_li = local_ids; search_id_li = local_ids;
} if (local_ids.length > 0)
}); $journals_sess.entry.qry__status = 'done';
});
}
} }
} catch (e) {
if (log_lvl) console.warn('Journal Fast Path failed.', e);
} }
} }
if (browser) { // 4. REVALIDATE: API Request
window.parent.postMessage( try {
{ journal_id: $journals_slct?.journal_id ?? null }, const results = await journals_func.qry__journal_entry({
'*' api_cfg: $ae_api,
); journal_id: journal_id,
} person_id: null,
qry_str: qry_str || null,
qry_category_code: cat_code || null,
enabled: params.enabled,
hidden: params.hidden,
limit: params.limit,
log_lvl: 0
});
import { LoaderCircle } from '@lucide/svelte'; if (current_search_id === last_search_id) {
const api_results = results || [];
const api_ids = api_results
.map((e: any) => e.id || e.journal_entry_id)
.filter(Boolean);
// Protect UI cache if API returns empty during revalidation
if (
api_ids.length === 0 &&
local_ids.length > 0 &&
!remote_first &&
!qry_str
) {
untrack(() => {
$journals_sess.entry.qry__status = 'done';
});
return;
}
untrack(() => {
$journals_sess.entry_li = api_results;
search_id_li = api_ids;
$journals_sess.entry.qry__status = 'done';
});
if (log_lvl)
console.log(
`[Journal Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`
);
}
} catch (error) {
if (current_search_id === last_search_id) {
console.error('Journal revalidation failed:', error);
untrack(() => {
$journals_sess.entry.qry__status = 'error';
if (search_id_li.length === 0 && local_ids.length > 0) {
search_id_li = local_ids;
}
});
}
}
}
if (browser) {
window.parent.postMessage(
{ journal_id: $journals_slct?.journal_id ?? null },
'*'
);
}
import { LoaderCircle } from '@lucide/svelte';
</script> </script>
<svelte:head> <svelte:head>
@@ -300,9 +296,8 @@
{#if $lq__journal_obj === undefined} {#if $lq__journal_obj === undefined}
<div <div
class="flex flex-col items-center justify-center p-20 opacity-50 text-center" class="flex flex-col items-center justify-center p-20 text-center opacity-50">
> <LoaderCircle size="3em" class="mx-auto mb-4 animate-spin" />
<LoaderCircle size="3em" class="animate-spin mb-4 mx-auto" />
<p class="text-xl">Loading Journal...</p> <p class="text-xl">Loading Journal...</p>
</div> </div>
{:else if $ae_loc.person_id == $lq__journal_obj?.person_id} {:else if $ae_loc.person_id == $lq__journal_obj?.person_id}
@@ -310,32 +305,27 @@
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
on_show_export={() => (show_export_modal = true)} on_show_export={() => (show_export_modal = true)}
on_show_import={() => (show_import_modal = true)} on_show_import={() => (show_import_modal = true)} />
/>
<Journal_entry_obj_li_wrapper <Journal_entry_obj_li_wrapper
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
show_found_header={false} show_found_header={false}
{log_lvl} {log_lvl} />
/>
<AeCompModalJournalExport <AeCompModalJournalExport
bind:open={show_export_modal} bind:open={show_export_modal}
entries={$lq__journal_entry_obj_li ?? []} entries={$lq__journal_entry_obj_li ?? []}
journal={$lq__journal_obj} journal={$lq__journal_obj}
on_close={() => (show_export_modal = false)} on_close={() => (show_export_modal = false)} />
/>
<AeCompModalJournalImport <AeCompModalJournalImport
bind:open={show_import_modal} bind:open={show_import_modal}
on_close={() => (show_import_modal = false)} on_close={() => (show_import_modal = false)}
on_import_complete={handle_import_complete} on_import_complete={handle_import_complete} />
/>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You must be logged in as the owner to view this Journal. You must be logged in as the owner to view this Journal.
</p> </p>

View File

@@ -1,264 +1,256 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').PageData} */ /** @type {import('./$types').PageData} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
// *** Import Svelte specific // *** Import Svelte specific
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
// *** Import other supporting libraries // *** Import other supporting libraries
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
// import type { key_val } from '$lib/ae_stores'; // import type { key_val } from '$lib/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
// import { core_func } from '$lib/ae_core/ae_core_functions'; // import { core_func } from '$lib/ae_core/ae_core_functions';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { import { ae_loc, ae_sess, ae_api, ae_trig } from '$lib/stores/ae_stores';
ae_loc, import {
ae_sess, journals_loc,
ae_api, journals_sess,
ae_trig, journals_slct,
} from '$lib/stores/ae_stores'; journals_prom,
import { journals_trig
journals_loc, } from '$lib/ae_journals/ae_journals_stores';
journals_sess,
journals_slct,
journals_prom,
journals_trig
} from '$lib/ae_journals/ae_journals_stores';
import Journal_entry_view from './../../../ae_comp__journal_entry_obj_id_view.svelte'; import Journal_entry_view from './../../../ae_comp__journal_entry_obj_id_view.svelte';
// import Element_data_store from '$lib/elements/element_data_store.svelte'; // import Element_data_store from '$lib/elements/element_data_store.svelte';
import AeCompModalJournalExport from '../../../ae_comp__modal_journal_export.svelte'; import AeCompModalJournalExport from '../../../ae_comp__modal_journal_export.svelte';
interface Props { interface Props {
data: any; data: any;
}
let { data }: Props = $props();
// let ae_promises: key_val = {};
// let ae_tmp: key_val = {};
// let ae_triggers: key_val = {};
// Variables
// *** Quickly pull out data from parent(s)
let ae_acct = $derived(data[data.account_id]);
let show_export_modal = $state(false);
$effect(() => {
if (!ae_acct) return;
if (log_lvl) console.log(`ae_acct = `, ae_acct);
untrack(() => {
$journals_slct.journal_id = ae_acct.slct.journal_id;
});
});
let lq__journal_obj = $derived(
liveQuery(async () => {
let results = await db_journals.journal.get(
$journals_slct?.journal_id ?? ''
); // null or undefined does not reset things like '' does
// Check if results are different than the current session version stored under $journals_slct
if ($journals_slct.journal_obj && results) {
if (
JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(results)
) {
$journals_slct.journal_obj = { ...results };
}
}
return results;
})
);
$effect(() => {
if (log_lvl) {
console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`
);
console.log(`lq__journal_obj: results = `, lq__journal_obj);
if ($journals_slct.journal_obj && lq__journal_obj) {
if (
JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(lq__journal_obj)
) {
console.log(
`Session slct stored version has changed for ID = ${$journals_slct.journal_id}`,
$journals_slct.journal_obj
);
} else {
console.log(
`Session slct stored version has not changed for ID = ${$journals_slct.journal_id}`
);
}
}
} }
});
let { data }: Props = $props(); let lq__journal_obj_li = $derived(
liveQuery(async () => {
let results = await db_journals.journal
.where('person_id')
.equals($ae_loc.person_id)
.reverse()
.sortBy('tmp_sort_2');
// let ae_promises: key_val = {}; // Check if results are different than the current session version stored under $journals_slct
// let ae_tmp: key_val = {}; if (
// let ae_triggers: key_val = {}; $journals_slct.journal_obj_li &&
JSON.stringify($journals_slct.journal_obj_li) !==
JSON.stringify(results)
) {
$journals_slct.journal_obj_li = [...results];
}
// Variables return results;
// *** Quickly pull out data from parent(s) })
let ae_acct = $derived(data[data.account_id]); );
let show_export_modal = $state(false);
$effect(() => { $effect(() => {
if (!ae_acct) return; if (log_lvl) {
if (log_lvl) console.log(`ae_acct = `, ae_acct); console.log(`lq__journal_obj_li: person_id = ${$ae_loc.person_id}`);
untrack(() => { console.log(`lq__journal_obj_li: results = `, lq__journal_obj_li);
$journals_slct.journal_id = ae_acct.slct.journal_id; if (
}); $journals_slct.journal_obj_li &&
}); JSON.stringify($journals_slct.journal_obj_li) !==
let lq__journal_obj = $derived( JSON.stringify(lq__journal_obj_li)
liveQuery(async () => { ) {
let results = await db_journals.journal.get(
$journals_slct?.journal_id ?? ''
); // null or undefined does not reset things like '' does
// Check if results are different than the current session version stored under $journals_slct
if ($journals_slct.journal_obj && results) {
if (
JSON.stringify($journals_slct.journal_obj) !==
JSON.stringify(results)
) {
$journals_slct.journal_obj = { ...results };
}
}
return results;
})
);
$effect(() => {
if (log_lvl) {
console.log( console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}` `Session slct li stored version has changed for ID = ${$ae_loc.person_id}`,
$journals_slct.journal_obj_li
); );
console.log(`lq__journal_obj: results = `, lq__journal_obj); } else {
if ($journals_slct.journal_obj && lq__journal_obj) { if (log_lvl > 1) {
if ( console.log(
JSON.stringify($journals_slct.journal_obj) !== `Session slct li stored version has not changed for ID = ${$ae_loc.person_id}`
JSON.stringify(lq__journal_obj) );
) {
console.log(
`Session slct stored version has changed for ID = ${$journals_slct.journal_id}`,
$journals_slct.journal_obj
);
} else {
console.log(
`Session slct stored version has not changed for ID = ${$journals_slct.journal_id}`
);
}
} }
} }
}
});
// For some reason data.params.journal_entry_id (or whatever param) is not being passed to this page when loaded by a link from another page. This seems to be a bug with Svelte or SvelteKit. Hopefully fixed in a future version 5? 2024-11-06
// NOTE: This must remain reactive (in an effect) so it updates on same-route navigation.
$effect(() => {
if (!ae_acct) return;
untrack(() => {
$journals_slct.journal_entry_id = ae_acct.slct.journal_entry_id;
// $journals_slct.journal_entry_obj = ae_acct.slct.journal_entry_obj;
}); });
});
let lq__journal_obj_li = $derived( let lq__journal_entry_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal let results = await db_journals.journal_entry.get(
.where('person_id') $journals_slct.journal_entry_id ?? ''
.equals($ae_loc.person_id) ); // null or undefined does not reset things like '' does
.reverse()
.sortBy('tmp_sort_2');
// Check if results are different than the current session version stored under $journals_slct // Check if results are different than the current session version stored under $journals_slct
if ($journals_slct.journal_entry_obj && results) {
if ( if (
$journals_slct.journal_obj_li && JSON.stringify($journals_slct.journal_entry_obj) !==
JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(results)
JSON.stringify(results)
) { ) {
$journals_slct.journal_obj_li = [...results]; $journals_slct.journal_entry_obj = { ...results };
} }
}
return results; return results;
}) })
); );
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log(`lq__journal_obj_li: person_id = ${$ae_loc.person_id}`); console.log(
console.log(`lq__journal_obj_li: results = `, lq__journal_obj_li); `lq__journal_entry_obj: journal_entry_id = ${$journals_slct?.journal_entry_id}`
);
console.log(`lq__journal_entry_obj: results = `, lq__journal_entry_obj);
if ($journals_slct.journal_entry_obj && lq__journal_entry_obj) {
if ( if (
$journals_slct.journal_obj_li && JSON.stringify($journals_slct.journal_entry_obj) !==
JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(lq__journal_entry_obj)
JSON.stringify(lq__journal_obj_li)
) { ) {
console.log( console.log(
`Session slct li stored version has changed for ID = ${$ae_loc.person_id}`, `Session slct stored version has changed for ID = ${$journals_slct.journal_entry_id}`,
$journals_slct.journal_obj_li $journals_slct.journal_entry_obj
); );
} else { } else {
if (log_lvl > 1) {
console.log(
`Session slct li stored version has not changed for ID = ${$ae_loc.person_id}`
);
}
}
}
});
// For some reason data.params.journal_entry_id (or whatever param) is not being passed to this page when loaded by a link from another page. This seems to be a bug with Svelte or SvelteKit. Hopefully fixed in a future version 5? 2024-11-06
// NOTE: This must remain reactive (in an effect) so it updates on same-route navigation.
$effect(() => {
if (!ae_acct) return;
untrack(() => {
$journals_slct.journal_entry_id = ae_acct.slct.journal_entry_id;
// $journals_slct.journal_entry_obj = ae_acct.slct.journal_entry_obj;
});
});
let lq__journal_entry_obj = $derived(
liveQuery(async () => {
let results = await db_journals.journal_entry.get(
$journals_slct.journal_entry_id ?? ''
); // null or undefined does not reset things like '' does
// Check if results are different than the current session version stored under $journals_slct
if ($journals_slct.journal_entry_obj && results) {
if (
JSON.stringify($journals_slct.journal_entry_obj) !==
JSON.stringify(results)
) {
$journals_slct.journal_entry_obj = { ...results };
}
}
return results;
})
);
$effect(() => {
if (log_lvl) {
console.log(
`lq__journal_entry_obj: journal_entry_id = ${$journals_slct?.journal_entry_id}`
);
console.log(
`lq__journal_entry_obj: results = `,
lq__journal_entry_obj
);
if ($journals_slct.journal_entry_obj && lq__journal_entry_obj) {
if (
JSON.stringify($journals_slct.journal_entry_obj) !==
JSON.stringify(lq__journal_entry_obj)
) {
console.log(
`Session slct stored version has changed for ID = ${$journals_slct.journal_entry_id}`,
$journals_slct.journal_entry_obj
);
} else {
console.log(
`Session slct stored version has not changed for ID = ${$journals_slct.journal_entry_id}`
);
}
}
}
});
$effect(() => {
if (browser && $lq__journal_entry_obj?.journal_entry_id) {
// Start with the current KV or convert the LI to a KV if needed
let history_kv = {
...($journals_loc?.entry_view_history_kv ?? {})
};
// Add or update the current entry
const entry_id = $lq__journal_entry_obj?.journal_entry_id ?? 'NONE';
history_kv[entry_id] = {
id: entry_id,
name:
$lq__journal_entry_obj?.name ??
ae_util.iso_datetime_formatter(
$lq__journal_entry_obj?.created_on,
'datetime_iso_12_no_seconds'
),
url: `/journals/${$lq__journal_entry_obj?.journal_id ?? 'NONE'}/entry/${entry_id}`
};
console.log(`history_kv (before limiting) = `, history_kv);
// // Convert KV to array, sort by most recent (last updated), and limit to 15
// let history_li = Object.values(history_kv);
// console.log(`history_li (before limiting) = `, history_li);
// // If you want to keep the most recent 15, you can use the order of insertion.
// // To do this, remove the oldest if over 15.
// if (history_li.length > 15) {
// // Remove the oldest entries (by insertion order)
// // Get the keys in insertion order
// const keys = Object.keys(history_kv);
// const keys_to_remove = keys.slice(0, history_li.length - 15);
// for (const key of keys_to_remove) {
// delete history_kv[key];
// }
// }
// Only update if changed
if (
JSON.stringify(history_kv) !==
JSON.stringify($journals_loc?.entry_view_history_kv)
) {
$journals_loc.entry_view_history_kv = history_kv;
console.log( console.log(
`$journals_loc.entry_view_history_kv = `, `Session slct stored version has not changed for ID = ${$journals_slct.journal_entry_id}`
$journals_loc.entry_view_history_kv
); );
} else {
if (log_lvl > 1) {
console.log(
`$journals_loc.entry_view_history_kv has not changed.`
);
}
} }
// log_lvl = 1;
} }
}); }
});
$effect(() => {
if (browser && $lq__journal_entry_obj?.journal_entry_id) {
// Start with the current KV or convert the LI to a KV if needed
let history_kv = {
...($journals_loc?.entry_view_history_kv ?? {})
};
// Add or update the current entry
const entry_id = $lq__journal_entry_obj?.journal_entry_id ?? 'NONE';
history_kv[entry_id] = {
id: entry_id,
name:
$lq__journal_entry_obj?.name ??
ae_util.iso_datetime_formatter(
$lq__journal_entry_obj?.created_on,
'datetime_iso_12_no_seconds'
),
url: `/journals/${$lq__journal_entry_obj?.journal_id ?? 'NONE'}/entry/${entry_id}`
};
console.log(`history_kv (before limiting) = `, history_kv);
// // Convert KV to array, sort by most recent (last updated), and limit to 15
// let history_li = Object.values(history_kv);
// console.log(`history_li (before limiting) = `, history_li);
// // If you want to keep the most recent 15, you can use the order of insertion.
// // To do this, remove the oldest if over 15.
// if (history_li.length > 15) {
// // Remove the oldest entries (by insertion order)
// // Get the keys in insertion order
// const keys = Object.keys(history_kv);
// const keys_to_remove = keys.slice(0, history_li.length - 15);
// for (const key of keys_to_remove) {
// delete history_kv[key];
// }
// }
// Only update if changed
if (
JSON.stringify(history_kv) !==
JSON.stringify($journals_loc?.entry_view_history_kv)
) {
$journals_loc.entry_view_history_kv = history_kv;
console.log(
`$journals_loc.entry_view_history_kv = `,
$journals_loc.entry_view_history_kv
);
} else {
if (log_lvl > 1) {
console.log(
`$journals_loc.entry_view_history_kv has not changed.`
);
}
}
// log_lvl = 1;
}
});
</script> </script>
<!-- <svelte:head> <!-- <svelte:head>
@@ -274,22 +266,20 @@
class=" class="
ae_journals__journal_entry ae_journals__journal_entry
mx-auto mx-auto
flex flex-col grow gap-1 flex max-h-max min-h-full max-w-max
items-center
min-h-full
max-h-max
min-w-full min-w-full
max-w-max grow
flex-col
items-center
gap-1
space-y-2 space-y-2
" ">
>
<!-- {#if $lq__journal_entry_obj} --> <!-- {#if $lq__journal_entry_obj} -->
<Journal_entry_view <Journal_entry_view
{lq__journal_obj} {lq__journal_obj}
{lq__journal_obj_li} {lq__journal_obj_li}
{lq__journal_entry_obj} {lq__journal_entry_obj}
on_show_export={() => (show_export_modal = true)} on_show_export={() => (show_export_modal = true)} />
/>
<!-- {/if} --> <!-- {/if} -->
</section> </section>
@@ -297,12 +287,10 @@
bind:open={show_export_modal} bind:open={show_export_modal}
entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []} entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []}
journal={$lq__journal_obj} journal={$lq__journal_obj}
on_close={() => (show_export_modal = false)} on_close={() => (show_export_modal = false)} />
/>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You must be logged in as the owner to view this Journal Entry. You must be logged in as the owner to view this Journal Entry.
</p> </p>

View File

@@ -1,24 +1,19 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_ai_tools.svelte * ae_comp__journal_entry_ai_tools.svelte
* Journal-specific wrapper for the generic AE_AITools. * Journal-specific wrapper for the generic AE_AITools.
* Handles layout/positioning and specific save behavior for journal entries. * Handles layout/positioning and specific save behavior for journal entries.
*/ */
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte'; import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
interface Props { interface Props {
content: string; content: string;
summary: string; // Bindable summary: string; // Bindable
on_save: () => void; on_save: () => void;
log_lvl?: number; log_lvl?: number;
} }
let { let { content, summary = $bindable(), on_save, log_lvl = 0 }: Props = $props();
content,
summary = $bindable(),
on_save,
log_lvl = 0
}: Props = $props();
</script> </script>
<div class="journal-entry-ai-tools absolute top-2 right-2 z-10"> <div class="journal-entry-ai-tools absolute top-2 right-2 z-10">
@@ -29,6 +24,5 @@
summary = newSummary; summary = newSummary;
on_save(); on_save();
}} }}
{log_lvl} {log_lvl} />
/>
</div> </div>

View File

@@ -1,51 +1,50 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_editor.svelte * ae_comp__journal_entry_editor.svelte
* Extracted 2026-01-08 to modularize the massive Journal Entry view. * Extracted 2026-01-08 to modularize the massive Journal Entry view.
* Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes. * Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes.
*/ */
import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte'; import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
journal: ae_Journal; journal: ae_Journal;
tmp_entry_obj: any; // Bindable tmp_entry_obj: any; // Bindable
editor_view?: any; // Bindable editor_view?: any; // Bindable
has_changed: boolean; has_changed: boolean;
updated_idb: boolean; updated_idb: boolean;
on_save: () => void; on_save: () => void;
on_force_reset?: () => void; on_force_reset?: () => void;
} }
let { let {
entry, entry,
journal, journal,
tmp_entry_obj = $bindable(), tmp_entry_obj = $bindable(),
editor_view = $bindable(), editor_view = $bindable(),
has_changed, has_changed,
updated_idb, updated_idb,
on_save, on_save,
on_force_reset on_force_reset
}: Props = $props(); }: Props = $props();
const is_editing = $derived( const is_editing = $derived(
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current' $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'
); );
</script> </script>
<div <div
class="journal-entry-editor-wrapper grow w-full flex flex-col items-center" class="journal-entry-editor-wrapper flex w-full grow flex-col items-center">
>
{#if !is_editing} {#if !is_editing}
<!-- VIEW MODE --> <!-- VIEW MODE -->
<div class="w-full max-w-6xl p-4 prose dark:prose-invert"> <div class="prose dark:prose-invert w-full max-w-6xl p-4">
{@html tmp_entry_obj?.content_md_html || ''} {@html tmp_entry_obj?.content_md_html || ''}
</div> </div>
{:else} {:else}
@@ -53,10 +52,9 @@
{#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted} {#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted}
<!-- Decryption Required Message --> <!-- Decryption Required Message -->
<div <div
class="w-full max-w-6xl p-4 bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 rounded-lg border border-error-500 flex flex-col gap-4" class="bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 border-error-500 flex w-full max-w-6xl flex-col gap-4 rounded-lg border p-4">
>
<div class="space-y-2"> <div class="space-y-2">
<div class="font-bold flex items-center gap-2"> <div class="flex items-center gap-2 font-bold">
<LockKeyhole size="1.25em" /> <LockKeyhole size="1.25em" />
Decryption Required Decryption Required
</div> </div>
@@ -65,16 +63,15 @@
</p> </p>
{#if tmp_entry_obj?.content === false} {#if tmp_entry_obj?.content === false}
<p <p
class="text-xs font-bold text-error-500 uppercase tracking-widest" class="text-error-500 text-xs font-bold tracking-widest uppercase">
>
Decryption failed. Incorrect passcode. Decryption failed. Incorrect passcode.
</p> </p>
{/if} {/if}
</div> </div>
{#if $ae_loc.edit_mode && on_force_reset} {#if $ae_loc.edit_mode && on_force_reset}
<div class="pt-4 border-t border-error-500/20"> <div class="border-error-500/20 border-t pt-4">
<p class="text-xs mb-2 opacity-70 italic"> <p class="mb-2 text-xs italic opacity-70">
Passcode lost? You can force a reset to plain text, Passcode lost? You can force a reset to plain text,
but all currently encrypted data will be permanently but all currently encrypted data will be permanently
deleted. deleted.
@@ -82,8 +79,7 @@
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold" class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold"
onclick={on_force_reset} onclick={on_force_reset}>
>
<RefreshCcw size="1.1em" class="mr-2" /> Force Reset to <RefreshCcw size="1.1em" class="mr-2" /> Force Reset to
Plain Text Plain Text
</button> </button>
@@ -98,14 +94,12 @@
bind:editor_view bind:editor_view
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
placeholder="Write using Markdown..." placeholder="Write using Markdown..."
class_li="p-2 preset-outlined-warning-300-700 shadow-lg rounded-lg w-full max-w-6xl bg-surface-50 dark:bg-surface-800" class_li="p-2 preset-outlined-warning-300-700 shadow-lg rounded-lg w-full max-w-6xl bg-surface-50 dark:bg-surface-800" />
/>
{:else} {:else}
<textarea <textarea
bind:value={tmp_entry_obj.content} bind:value={tmp_entry_obj.content}
class="textarea grow w-full max-w-6xl p-4 font-mono shadow-lg rounded-lg border-orange-500/30 h-[500px] whitespace-pre-wrap break-words" class="textarea h-[500px] w-full max-w-6xl grow rounded-lg border-orange-500/30 p-4 font-mono break-words whitespace-pre-wrap shadow-lg"
placeholder="Edit content..." placeholder="Edit content..."></textarea>
></textarea>
{/if} {/if}
<!-- Floating Save Button --> <!-- Floating Save Button -->
@@ -113,9 +107,8 @@
type="button" type="button"
onclick={on_save} onclick={on_save}
disabled={!has_changed} disabled={!has_changed}
class="btn btn-sm md:btn-md lg:btn-lg fixed top-72 right-6 min-w-32 preset-filled-success shadow-xl z-20 transition-all" class="btn btn-sm md:btn-md lg:btn-lg preset-filled-success fixed top-72 right-6 z-20 min-w-32 shadow-xl transition-all"
class:hidden={!has_changed} class:hidden={!has_changed}>
>
<Save size="1.2em" class="mr-2" /> Save <Save size="1.2em" class="mr-2" /> Save
</button> </button>
@@ -124,16 +117,14 @@
type="button" type="button"
onclick={on_save} onclick={on_save}
disabled={!has_changed} disabled={!has_changed}
class="btn preset-tonal-warning hover:preset-filled-warning-500 w-full max-w-96 mt-4" class="btn preset-tonal-warning hover:preset-filled-warning-500 mt-4 w-full max-w-96"
class:invisible={!has_changed} class:invisible={!has_changed}>
>
<Save size="1.2em" class="mr-2" /> Save Changes <Save size="1.2em" class="mr-2" /> Save Changes
</button> </button>
{#if updated_idb} {#if updated_idb}
<p <p
class="text-xs text-error-500 mt-2 font-bold animate-pulse uppercase tracking-widest" class="text-error-500 mt-2 animate-pulse text-xs font-bold tracking-widest uppercase">
>
IDB object updated since last load! IDB object updated since last load!
</p> </p>
{/if} {/if}

View File

@@ -1,75 +1,85 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_header.svelte * ae_comp__journal_entry_header.svelte
* Standardized Journal Entry Header. * Standardized Journal Entry Header.
* Manages name, sync status, and triggers the modular config. * Manages name, sync status, and triggers the modular config.
*/ */
import { ChevronLeft, CircleCheck, CircleX, Eye, Fingerprint, LoaderCircle, LockKeyhole, LockKeyholeOpen, Pencil, RefreshCw, Save, Settings } from '@lucide/svelte'; import {
import { ae_util } from '$lib/ae_utils/ae_utils'; ChevronLeft,
import { CircleCheck,
journals_loc, CircleX,
journals_sess Eye,
} from '$lib/ae_journals/ae_journals_stores'; Fingerprint,
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; LoaderCircle,
LockKeyhole,
LockKeyholeOpen,
Pencil,
RefreshCw,
Save,
Settings
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
journals_loc,
journals_sess
} from '$lib/ae_journals/ae_journals_stores';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
journal: ae_Journal; journal: ae_Journal;
journals_li?: ae_Journal[]; journals_li?: ae_Journal[];
tmp_entry_obj: any; // Bindable tmp_entry_obj: any; // Bindable
has_changed: boolean; has_changed: boolean;
save_status?: 'saved' | 'unsaved' | 'saving'; save_status?: 'saved' | 'unsaved' | 'saving';
on_save: () => void; on_save: () => void;
on_decrypt: () => void; on_decrypt: () => void;
on_show_config: () => void; on_show_config: () => void;
log_lvl?: number; log_lvl?: number;
} }
let { let {
entry, entry,
journal, journal,
tmp_entry_obj = $bindable(), tmp_entry_obj = $bindable(),
has_changed, has_changed,
save_status = 'saved', save_status = 'saved',
on_save, on_save,
on_decrypt, on_decrypt,
on_show_config, on_show_config,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
const is_decrypted = $derived( const is_decrypted = $derived(
$journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted === $journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted === true
true );
);
function toggle_edit_mode() {
function toggle_edit_mode() { const isEditing =
const isEditing = $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current';
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'; if (isEditing) {
if (isEditing) { if (has_changed) on_save();
if (has_changed) on_save(); else $journals_loc.entry.edit_kv[entry.journal_entry_id] = false;
else $journals_loc.entry.edit_kv[entry.journal_entry_id] = false; } else {
} else { $journals_loc.entry.edit_kv[entry.journal_entry_id] = 'current';
$journals_loc.entry.edit_kv[entry.journal_entry_id] = 'current';
}
} }
}
</script> </script>
<header <header
class="flex flex-col md:flex-row items-center justify-between gap-4 p-3 class="flex w-full flex-col items-center justify-between gap-4 rounded-xl
bg-gray-50 dark:bg-gray-800 border border-gray-200
border border-gray-200 dark:border-gray-700 bg-gray-50 p-3 shadow-sm
rounded-xl shadow-sm w-full" md:flex-row dark:border-gray-700 dark:bg-gray-800">
> <div class="flex w-full items-center gap-3 md:w-auto">
<div class="flex items-center gap-3 w-full md:w-auto">
<a <a
href="/journals/{journal.journal_id}" href="/journals/{journal.journal_id}"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
title="Back to Journal" title="Back to Journal">
>
<ChevronLeft size="1.2em" /> <ChevronLeft size="1.2em" />
</a> </a>
<div class="flex items-center gap-2 grow"> <div class="flex grow items-center gap-2">
<button <button
type="button" type="button"
onclick={toggle_edit_mode} onclick={toggle_edit_mode}
@@ -77,12 +87,10 @@
$journals_loc.entry.edit_kv[entry.journal_entry_id] === $journals_loc.entry.edit_kv[entry.journal_entry_id] ===
'current' 'current'
? 'preset-filled-success' ? 'preset-filled-success'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}">
>
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'} {#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
{#if has_changed}<Save size="1.2em" />{:else}<Eye {#if has_changed}<Save size="1.2em" />{:else}<Eye
size="1.2em" size="1.2em" />{/if}
/>{/if}
{:else} {:else}
<Pencil size="1.2em" /> <Pencil size="1.2em" />
{/if} {/if}
@@ -92,12 +100,11 @@
<input <input
type="text" type="text"
bind:value={tmp_entry_obj.name} bind:value={tmp_entry_obj.name}
class="input input-sm font-bold text-lg grow md:min-w-[300px] border-none bg-transparent focus:ring-2 focus:ring-primary-500" class="input input-sm focus:ring-primary-500 grow border-none bg-transparent text-lg font-bold focus:ring-2 md:min-w-[300px]"
placeholder="Entry Title..." placeholder="Entry Title..."
onchange={on_save} onchange={on_save} />
/>
{:else} {:else}
<h2 class="text-base md:text-lg font-bold truncate max-w-md"> <h2 class="max-w-md truncate text-base font-bold md:text-lg">
{entry.name || {entry.name ||
ae_util.iso_datetime_formatter( ae_util.iso_datetime_formatter(
entry.created_on, entry.created_on,
@@ -108,12 +115,11 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2 w-full md:w-auto justify-end"> <div class="flex w-full items-center justify-end gap-2 md:w-auto">
<!-- Auto-Save indicator --> <!-- Auto-Save indicator -->
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'} {#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
<div <div
class="flex items-center gap-1 px-2 border-r border-surface-500/20 mr-1" class="border-surface-500/20 mr-1 flex items-center gap-1 border-r px-2">
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm {$journals_loc.entry.auto_save class="btn-icon btn-icon-sm {$journals_loc.entry.auto_save
@@ -122,19 +128,16 @@
onclick={() => onclick={() =>
($journals_loc.entry.auto_save = ($journals_loc.entry.auto_save =
!$journals_loc.entry.auto_save)} !$journals_loc.entry.auto_save)}
title="Toggle Auto Save" title="Toggle Auto Save">
>
<RefreshCw size="1em" /> <RefreshCw size="1em" />
</button> </button>
{#if $journals_loc.entry.auto_save} {#if $journals_loc.entry.auto_save}
{#if save_status === 'saving'}<LoaderCircle {#if save_status === 'saving'}<LoaderCircle
size="1em" size="1em"
class="animate-spin text-primary-500" class="text-primary-500 animate-spin" />
/>
{:else if save_status === 'saved'}<CircleCheck {:else if save_status === 'saved'}<CircleCheck
size="1em" size="1em"
class="text-success-500" class="text-success-500" />
/>
{:else}<CircleX size="1em" class="opacity-30" />{/if} {:else}<CircleX size="1em" class="opacity-30" />{/if}
{/if} {/if}
</div> </div>
@@ -145,25 +148,22 @@
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm transition-all {is_decrypted class="btn-icon btn-icon-sm transition-all {is_decrypted
? 'preset-filled-success shadow-lg shadow-success-500/20' ? 'preset-filled-success shadow-success-500/20 shadow-lg'
: 'preset-tonal-warning'}" : 'preset-tonal-warning'}"
onclick={on_decrypt} onclick={on_decrypt}
title={is_decrypted ? 'Lock Content' : 'Decrypt Content'} title={is_decrypted ? 'Lock Content' : 'Decrypt Content'}>
>
{#if is_decrypted}<LockKeyholeOpen {#if is_decrypted}<LockKeyholeOpen
size="1.2em" size="1.2em" />{:else}<LockKeyhole size="1.2em" />{/if}
/>{:else}<LockKeyhole size="1.2em" />{/if}
</button> </button>
{/if} {/if}
<div class="w-[1px] h-6 bg-surface-500/20 mx-1"></div> <div class="bg-surface-500/20 mx-1 h-6 w-[1px]"></div>
<!-- Unified Config Button --> <!-- Unified Config Button -->
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-primary font-bold" class="btn btn-sm preset-tonal-primary font-bold"
onclick={on_show_config} onclick={on_show_config}>
>
<Settings size="1.1em" class="mr-2" /> Config <Settings size="1.1em" class="mr-2" /> Config
</button> </button>
@@ -172,8 +172,7 @@
<button <button
type="button" type="button"
class="btn btn-sm preset-filled-primary" class="btn btn-sm preset-filled-primary"
onclick={on_save} onclick={on_save}>
>
<Save size="1.1em" class="mr-2" /> Save <Save size="1.1em" class="mr-2" /> Save
</button> </button>
{/if} {/if}

View File

@@ -1,25 +1,24 @@
<script lang="ts"> <script lang="ts">
/** /**
* JournalEntry_Metadata.svelte * JournalEntry_Metadata.svelte
* Extracted 2026-01-08 to modularize the massive Journal Entry God Component. * Extracted 2026-01-08 to modularize the massive Journal Entry God Component.
* Displays creation, update, and original datetime/timezone information. * Displays creation, update, and original datetime/timezone information.
*/ */
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import type { ae_JournalEntry } from '$lib/types/ae_types'; import type { ae_JournalEntry } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
} }
let { entry }: Props = $props(); let { entry }: Props = $props();
</script> </script>
<section class="journal-metadata w-full space-y-4"> <section class="journal-metadata w-full space-y-4">
<!-- System Timestamps --> <!-- System Timestamps -->
<div <div
class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1 text-xs text-surface-500" class="text-surface-500 flex flex-col items-center justify-between gap-2 px-1 text-xs sm:flex-row">
>
<div class="flex gap-4"> <div class="flex gap-4">
<span title="Creation date"> <span title="Creation date">
<span class="font-semibold">Created:</span> <span class="font-semibold">Created:</span>

View File

@@ -1,205 +1,206 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_obj_file_li.svelte * ae_comp__journal_entry_obj_file_li.svelte
* Manages and displays file attachments for Journal Entries. * Manages and displays file attachments for Journal Entries.
* Ported/Refactored 2026-01-26 to include View mode and strictly snake_case. * Ported/Refactored 2026-01-26 to include View mode and strictly snake_case.
*/ */
// *** Import Lucide Icons // *** Import Lucide Icons
import { Download, ExternalLink, FileUp, LoaderCircle, Paperclip, RefreshCw, Trash2 } from '@lucide/svelte'; import {
// *** Import Aether specific variables and functions Download,
import type { key_val } from '$lib/stores/ae_stores'; ExternalLink,
import { ae_util } from '$lib/ae_utils/ae_utils'; FileUp,
import { core_func } from '$lib/ae_core/ae_core_functions'; LoaderCircle,
import { api } from '$lib/api/api'; Paperclip,
import { RefreshCw,
ae_snip, Trash2
ae_loc, } from '@lucide/svelte';
ae_sess, // *** Import Aether specific variables and functions
ae_api, import type { key_val } from '$lib/stores/ae_stores';
ae_trig, import { ae_util } from '$lib/ae_utils/ae_utils';
slct, import { core_func } from '$lib/ae_core/ae_core_functions';
slct_trigger import { api } from '$lib/api/api';
} from '$lib/stores/ae_stores'; import {
import { ae_snip,
journals_loc, ae_loc,
journals_sess, ae_sess,
journals_slct, ae_api,
journals_trig, ae_trig,
journals_prom slct,
} from '$lib/ae_journals/ae_journals_stores'; slct_trigger
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_trig,
journals_prom
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import Comp_hosted_files_upload from '$lib/ae_core/ae_comp__hosted_files_upload.svelte'; import Comp_hosted_files_upload from '$lib/ae_core/ae_comp__hosted_files_upload.svelte';
import Element_manage_hosted_file_li_wrap from '$lib/elements/element_manage_hosted_file_li_all.svelte'; import Element_manage_hosted_file_li_wrap from '$lib/elements/element_manage_hosted_file_li_all.svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte'; import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
link_to_type: string; link_to_type: string;
link_to_id: string; link_to_id: string;
lq__journal_entry_obj: any; lq__journal_entry_obj: any;
}
let {
log_lvl = 0,
link_to_type,
link_to_id,
lq__journal_entry_obj
}: Props = $props();
// *** State
let ae_promises: Record<string, any> = $state({});
let upload_complete: boolean = $state(false);
// Selection state for "Select Existing" mode
let slct_hosted_file_kv: key_val = $state({});
let slct_hosted_file_id: string | null = $state(null);
let slct_hosted_file_obj: any = $state(null);
// Selection state for "Upload" mode
let slct_hosted_file_id_li: string[] = $state([]);
let slct_hosted_file_obj_li: any[] = $state([]);
// Derived: Unified file list from both legacy data_json and new linked_li_json
let unified_file_li = $derived.by(() => {
const entry = $lq__journal_entry_obj;
if (!entry) return [];
let files: any[] = [];
// 1. Check new linked_li_json first
if (entry.linked_li_json && Array.isArray(entry.linked_li_json)) {
files = [...entry.linked_li_json];
} }
let { // 2. Merge with legacy hosted_file_kv if not already present
log_lvl = 0, if (entry.data_json?.hosted_file_kv) {
link_to_type, Object.entries(entry.data_json.hosted_file_kv).forEach(
link_to_id, ([id, obj]: [string, any]) => {
lq__journal_entry_obj if (
}: Props = $props(); !files.find(
(f) =>
// *** State (f.hosted_file_id || f.id || f.hosted_file_id) ===
let ae_promises: Record<string, any> = $state({}); id
let upload_complete: boolean = $state(false); )
) {
// Selection state for "Select Existing" mode files.push({ ...obj, id: id });
let slct_hosted_file_kv: key_val = $state({});
let slct_hosted_file_id: string | null = $state(null);
let slct_hosted_file_obj: any = $state(null);
// Selection state for "Upload" mode
let slct_hosted_file_id_li: string[] = $state([]);
let slct_hosted_file_obj_li: any[] = $state([]);
// Derived: Unified file list from both legacy data_json and new linked_li_json
let unified_file_li = $derived.by(() => {
const entry = $lq__journal_entry_obj;
if (!entry) return [];
let files: any[] = [];
// 1. Check new linked_li_json first
if (entry.linked_li_json && Array.isArray(entry.linked_li_json)) {
files = [...entry.linked_li_json];
}
// 2. Merge with legacy hosted_file_kv if not already present
if (entry.data_json?.hosted_file_kv) {
Object.entries(entry.data_json.hosted_file_kv).forEach(
([id, obj]: [string, any]) => {
if (
!files.find(
(f) =>
(f.hosted_file_id ||
f.id ||
f.hosted_file_id) === id
)
) {
files.push({ ...obj, id: id });
}
} }
);
}
return files;
});
async function update_journal_entry(updated_files: any[]) {
let data_kv: key_val = {
linked_li_json: JSON.stringify(updated_files),
// Maintain data_json.hosted_file_kv for backward compatibility
data_json: {
...$lq__journal_entry_obj?.data_json,
hosted_file_kv: Object.fromEntries(
updated_files.map((f) => [
f.hosted_file_id || f.id || f.hosted_file_id,
f
])
)
} }
};
try {
await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
data_kv: data_kv,
log_lvl: 1
});
} catch (error) {
console.error('Error updating journal entry files:', error);
}
}
// *** Effects for File Management
// Handle "Select Existing" completion
$effect(() => {
if (
$lq__journal_entry_obj &&
slct_hosted_file_id &&
slct_hosted_file_obj
) {
const new_file = {
...slct_hosted_file_obj,
id: slct_hosted_file_id
};
const updated_li = [...unified_file_li, new_file];
slct_hosted_file_id = null;
slct_hosted_file_obj = null;
update_journal_entry(updated_li);
}
});
// Handle "Upload" completion
$effect(() => {
if (
$lq__journal_entry_obj &&
upload_complete &&
slct_hosted_file_id_li.length
) {
const new_files = slct_hosted_file_id_li.map((id) => ({
...slct_hosted_file_kv[id],
id: id
}));
const updated_li = [...unified_file_li, ...new_files];
slct_hosted_file_id_li = [];
upload_complete = false;
update_journal_entry(updated_li);
}
});
async function handle_remove_file(file_id: string) {
if (!confirm('Are you sure you want to remove this file attachment?'))
return;
const updated_li = unified_file_li.filter(
(f) => (f.hosted_file_id || f.id || f.hosted_file_id) !== file_id
); );
// Also perform physical/orphan deletion if admin
if ($ae_loc.administrator_access) {
await core_func.delete_ae_obj_id__hosted_file({
api_cfg: $ae_api,
hosted_file_id: file_id,
link_to_type: link_to_type,
link_to_id: link_to_id,
rm_orphan: true,
fake_delete: false,
log_lvl
});
}
await update_journal_entry(updated_li);
} }
return files;
});
async function update_journal_entry(updated_files: any[]) {
let data_kv: key_val = {
linked_li_json: JSON.stringify(updated_files),
// Maintain data_json.hosted_file_kv for backward compatibility
data_json: {
...$lq__journal_entry_obj?.data_json,
hosted_file_kv: Object.fromEntries(
updated_files.map((f) => [
f.hosted_file_id || f.id || f.hosted_file_id,
f
])
)
}
};
try {
await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
data_kv: data_kv,
log_lvl: 1
});
} catch (error) {
console.error('Error updating journal entry files:', error);
}
}
// *** Effects for File Management
// Handle "Select Existing" completion
$effect(() => {
if ($lq__journal_entry_obj && slct_hosted_file_id && slct_hosted_file_obj) {
const new_file = {
...slct_hosted_file_obj,
id: slct_hosted_file_id
};
const updated_li = [...unified_file_li, new_file];
slct_hosted_file_id = null;
slct_hosted_file_obj = null;
update_journal_entry(updated_li);
}
});
// Handle "Upload" completion
$effect(() => {
if (
$lq__journal_entry_obj &&
upload_complete &&
slct_hosted_file_id_li.length
) {
const new_files = slct_hosted_file_id_li.map((id) => ({
...slct_hosted_file_kv[id],
id: id
}));
const updated_li = [...unified_file_li, ...new_files];
slct_hosted_file_id_li = [];
upload_complete = false;
update_journal_entry(updated_li);
}
});
async function handle_remove_file(file_id: string) {
if (!confirm('Are you sure you want to remove this file attachment?'))
return;
const updated_li = unified_file_li.filter(
(f) => (f.hosted_file_id || f.id || f.hosted_file_id) !== file_id
);
// Also perform physical/orphan deletion if admin
if ($ae_loc.administrator_access) {
await core_func.delete_ae_obj_id__hosted_file({
api_cfg: $ae_api,
hosted_file_id: file_id,
link_to_type: link_to_type,
link_to_id: link_to_id,
rm_orphan: true,
fake_delete: false,
log_lvl
});
}
await update_journal_entry(updated_li);
}
</script> </script>
<section class="ae_section journal_entry_files w-full space-y-4 my-2"> <section class="ae_section journal_entry_files my-2 w-full space-y-4">
<!-- Header --> <!-- Header -->
<div <div
class="flex items-center justify-between border-b border-surface-500/20 pb-2" class="border-surface-500/20 flex items-center justify-between border-b pb-2">
>
<h3 class="h3 flex items-center gap-2 text-lg font-bold"> <h3 class="h3 flex items-center gap-2 text-lg font-bold">
<Paperclip size="1.1em" /> <Paperclip size="1.1em" />
Attachments Attachments
{#if unified_file_li.length} {#if unified_file_li.length}
<span class="badge preset-tonal-surface text-xs" <span class="badge preset-tonal-surface text-xs"
>{unified_file_li.length}</span >{unified_file_li.length}</span>
>
{/if} {/if}
</h3> </h3>
@@ -212,8 +213,7 @@
$ae_sess.files.add_to_use_files_method === 'upload' $ae_sess.files.add_to_use_files_method === 'upload'
? 'select' ? 'select'
: 'upload'; : 'upload';
}} }}>
>
<RefreshCw size="1em" class="mr-2" /> <RefreshCw size="1em" class="mr-2" />
{$ae_sess.files.add_to_use_files_method === 'select' {$ae_sess.files.add_to_use_files_method === 'select'
? 'Switch to Upload' ? 'Switch to Upload'
@@ -224,14 +224,13 @@
<!-- File Grid --> <!-- File Grid -->
{#if unified_file_li.length} {#if unified_file_li.length}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{#each unified_file_li as file (file.hosted_file_id ?? file.id)} {#each unified_file_li as file (file.hosted_file_id ?? file.id)}
{@const file_id = {@const file_id =
file.hosted_file_id || file.id || file.hosted_file_id} file.hosted_file_id || file.id || file.hosted_file_id}
<div <div
class="flex items-center justify-between p-3 rounded-xl bg-surface-50-950 border border-surface-500/10 group hover:border-primary-500 transition-all shadow-sm" class="bg-surface-50-950 border-surface-500/10 group hover:border-primary-500 flex items-center justify-between rounded-xl border p-3 shadow-sm transition-all">
> <div class="flex grow items-center gap-3 overflow-hidden">
<div class="flex items-center gap-3 overflow-hidden grow">
<AE_Comp_Hosted_Files_Download_Button <AE_Comp_Hosted_Files_Download_Button
hosted_file_id={file_id} hosted_file_id={file_id}
hosted_file_obj={file} hosted_file_obj={file}
@@ -241,18 +240,16 @@
show_divider={true} show_divider={true}
max_filename={25} max_filename={25}
show_direct_download={$ae_loc.trusted_access && show_direct_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode} $ae_loc.edit_mode} />
/>
</div> </div>
<div class="flex items-center gap-1 ml-2"> <div class="ml-2 flex items-center gap-1">
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error" class="btn btn-sm preset-tonal-error"
onclick={() => handle_remove_file(file_id)} onclick={() => handle_remove_file(file_id)}
title="Remove attachment" title="Remove attachment">
>
<Trash2 size="1.1em" /> <Trash2 size="1.1em" />
</button> </button>
{/if} {/if}
@@ -262,8 +259,7 @@
</div> </div>
{:else if !$ae_loc.edit_mode} {:else if !$ae_loc.edit_mode}
<p <p
class="text-sm text-surface-500 italic p-4 text-center bg-surface-500/5 rounded-xl" class="text-surface-500 bg-surface-500/5 rounded-xl p-4 text-center text-sm italic">
>
No files attached to this entry. No files attached to this entry.
</p> </p>
{/if} {/if}
@@ -271,8 +267,7 @@
<!-- Edit/Management Tools --> <!-- Edit/Management Tools -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="mt-4 p-4 rounded-2xl bg-surface-500/5 border-2 border-dashed border-surface-500/20" class="bg-surface-500/5 border-surface-500/20 mt-4 rounded-2xl border-2 border-dashed p-4">
>
{#if $ae_sess.files.add_to_use_files_method === 'upload'} {#if $ae_sess.files.add_to_use_files_method === 'upload'}
<Comp_hosted_files_upload <Comp_hosted_files_upload
{link_to_type} {link_to_type}
@@ -282,13 +277,11 @@
bind:hosted_file_obj_li={slct_hosted_file_obj_li} bind:hosted_file_obj_li={slct_hosted_file_obj_li}
bind:hosted_file_obj_kv={slct_hosted_file_kv} bind:hosted_file_obj_kv={slct_hosted_file_kv}
accept="*/*" accept="*/*"
class_li="!max-w-none" class_li="!max-w-none">
>
{#snippet label()} {#snippet label()}
<div class="flex flex-col items-center gap-2 py-2"> <div class="flex flex-col items-center gap-2 py-2">
<div <div
class="p-3 rounded-full bg-primary-500/10 text-primary-500" class="bg-primary-500/10 text-primary-500 rounded-full p-3">
>
<FileUp size="1.8em" /> <FileUp size="1.8em" />
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -302,7 +295,7 @@
</Comp_hosted_files_upload> </Comp_hosted_files_upload>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-2">
<p class="text-sm font-bold opacity-70 ml-1"> <p class="ml-1 text-sm font-bold opacity-70">
Select from existing hosted files: Select from existing hosted files:
</p> </p>
<Element_manage_hosted_file_li_wrap <Element_manage_hosted_file_li_wrap
@@ -313,8 +306,7 @@
class_li={''} class_li={''}
bind:slct_hosted_file_kv bind:slct_hosted_file_kv
bind:slct_hosted_file_id bind:slct_hosted_file_id
bind:slct_hosted_file_obj bind:slct_hosted_file_obj />
/>
</div> </div>
{/if} {/if}
</div> </div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
container_class_li?: string | Array<string>; container_class_li?: string | Array<string>;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_entry_obj_li: any; lq__journal_entry_obj_li: any;
show_found_header?: boolean; show_found_header?: boolean;
log_lvl?: number; log_lvl?: number;
} }
let { let {
container_class_li = '', container_class_li = '',
lq__journal_obj, lq__journal_obj,
lq__journal_entry_obj_li, lq__journal_entry_obj_li,
show_found_header = true, show_found_header = true,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
// *** Import other supporting libraries // *** Import other supporting libraries
import { LoaderCircle } from '@lucide/svelte'; import { LoaderCircle } from '@lucide/svelte';
// *** Import Aether specific components // *** Import Aether specific components
import Journal_entry_obj_li from './ae_comp__journal_entry_obj_li.svelte'; import Journal_entry_obj_li from './ae_comp__journal_entry_obj_li.svelte';
</script> </script>
{#if $lq__journal_entry_obj_li} {#if $lq__journal_entry_obj_li}
@@ -26,11 +26,10 @@
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
{show_found_header} {show_found_header}
{log_lvl} {log_lvl} />
/>
{:else} {:else}
<div class="flex flex-col items-center justify-center p-10 opacity-50"> <div class="flex flex-col items-center justify-center p-10 opacity-50">
<LoaderCircle size="2em" class="animate-spin mb-2" /> <LoaderCircle size="2em" class="mb-2 animate-spin" />
<p>Loading entries...</p> <p>Loading entries...</p>
</div> </div>
{/if} {/if}

View File

@@ -1,92 +1,91 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
}
let { log_lvl = $bindable(0), lq__journal_obj }: Props = $props();
import {
ArrowDown01,
ArrowDown10,
ArrowDownUp,
BetweenVerticalEnd,
BetweenVerticalStart,
BookHeart,
BookImage,
Bookmark,
BookOpenText,
BriefcaseBusiness,
Check,
Copy,
Expand,
Eye,
EyeOff,
Flag,
FlagOff,
FilePlus,
Fingerprint,
Globe,
Library,
MessageSquareWarning,
Minus,
Notebook,
Pencil,
Plus,
RemoveFormatting,
SquareLibrary,
Shapes,
Share2,
ShieldCheck,
ShieldMinus,
Siren,
Skull,
Tags,
Target,
ToggleLeft,
ToggleRight,
Trash2,
TypeOutline,
X
} from '@lucide/svelte';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_prom,
journals_trig
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
// *** Functions and Logic
function handle_search_trigger() {
if ($journals_loc.entry.search_version === undefined) {
$journals_loc.entry.search_version = 0;
} }
$journals_loc.entry.search_version++;
}
let { log_lvl = $bindable(0), lq__journal_obj }: Props = $props(); function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
import { event.preventDefault();
ArrowDown01, fn(event);
ArrowDown10, };
ArrowDownUp, }
BetweenVerticalEnd,
BetweenVerticalStart,
BookHeart,
BookImage,
Bookmark,
BookOpenText,
BriefcaseBusiness,
Check,
Copy,
Expand,
Eye,
EyeOff,
Flag,
FlagOff,
FilePlus,
Fingerprint,
Globe,
Library,
MessageSquareWarning,
Minus,
Notebook,
Pencil,
Plus,
RemoveFormatting,
SquareLibrary,
Shapes,
Share2,
ShieldCheck,
ShieldMinus,
Siren,
Skull,
Tags,
Target,
ToggleLeft,
ToggleRight,
Trash2,
TypeOutline,
X
} from '@lucide/svelte';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_prom,
journals_trig
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
// *** Functions and Logic
function handle_search_trigger() {
if ($journals_loc.entry.search_version === undefined) {
$journals_loc.entry.search_version = 0;
}
$journals_loc.entry.search_version++;
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script> </script>
<div <div
class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2" class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2">
>
<!-- Search input form --> <!-- Search input form -->
<span class="flex flex-row flex-wrap items-center justify-center gap-1"> <span class="flex flex-row flex-wrap items-center justify-center gap-1">
<form <form
@@ -94,9 +93,8 @@
handle_search_trigger(); handle_search_trigger();
})} })}
autocomplete="off" autocomplete="off"
class="search_form flex flex-row flex-wrap gap-1 items-center justify-center" class="search_form flex flex-row flex-wrap items-center justify-center gap-1">
> <span class="hidden text-sm text-gray-500 lg:inline">
<span class="text-sm text-gray-500 hidden lg:inline">
Search: Search:
</span> </span>
<input <input
@@ -111,18 +109,16 @@
autocomplete="off" autocomplete="off"
class=" class="
input input-sm input input-sm
w-44 md:w-52 w-44 text-sm
text-sm md:w-52
" "
class:bg-red-200={$journals_sess.entry_li == null} class:bg-red-200={$journals_sess.entry_li == null}
class:dark:bg-red-800={$journals_sess.entry_li == null} class:dark:bg-red-800={$journals_sess.entry_li == null} />
/>
<button <button
type="submit" type="submit"
class="btn btn-sm preset-filled-primary transition" class="btn btn-sm preset-filled-primary transition"
title="Perform detailed search" title="Perform detailed search">
>
<Library size="1.25em" /> <Library size="1.25em" />
</button> </button>
@@ -142,12 +138,10 @@
hover:preset-filled-surface-500 hover:preset-filled-surface-500
transition-all transition-all
" "
title="Clear search query text" title="Clear search query text">
>
<RemoveFormatting <RemoveFormatting
size="1.25em" size="1.25em"
class="text-neutral-800/60 dark:text-neutral-50/60" class="text-neutral-800/60 dark:text-neutral-50/60" />
/>
<span class="hidden md:inline"> Clear </span> <span class="hidden md:inline"> Clear </span>
</button> </button>
</form> </form>
@@ -155,15 +149,14 @@
<!-- Give list of categories to base the new entry on --> <!-- Give list of categories to base the new entry on -->
<span class="flex flex-row items-center gap-2"> <span class="flex flex-row items-center gap-2">
<span class="text-sm text-gray-500 hidden md:inline"> Category: </span> <span class="hidden text-sm text-gray-500 md:inline"> Category: </span>
<select <select
class="select select-sm" class="select select-sm"
bind:value={$journals_loc.entry.qry__category_code} bind:value={$journals_loc.entry.qry__category_code}
onchange={(event) => { onchange={(event) => {
handle_search_trigger(); handle_search_trigger();
}} }}
title="Filter by category" title="Filter by category">
>
<option value="">All Categories</option> <option value="">All Categories</option>
{#each $lq__journal_obj?.cfg_json?.category_li as category (category.code)} {#each $lq__journal_obj?.cfg_json?.category_li as category (category.code)}
<option value={category.code}>{category.name}</option> <option value={category.code}>{category.name}</option>
@@ -173,8 +166,7 @@
<!-- Search Control Toggles --> <!-- Search Control Toggles -->
<span <span
class="flex flex-row flex-wrap items-center gap-2 border-l border-surface-300-700 pl-2" class="border-surface-300-700 flex flex-row flex-wrap items-center gap-2 border-l pl-2">
>
<!-- Global Search hidden until backend supports person_id in entries 2026-01-27 --> <!-- Global Search hidden until backend supports person_id in entries 2026-01-27 -->
<!-- <!--
<label <label
@@ -193,9 +185,8 @@
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<label <label
class="flex items-center gap-1 cursor-pointer" class="flex cursor-pointer items-center gap-1"
title="When enabled, search results are fetched directly from the server first." title="When enabled, search results are fetched directly from the server first.">
>
<span class="text-xs font-semibold text-gray-500"> <span class="text-xs font-semibold text-gray-500">
Remote First? Remote First?
</span> </span>
@@ -203,8 +194,7 @@
type="checkbox" type="checkbox"
bind:checked={$journals_loc.entry.qry__remote_first} bind:checked={$journals_loc.entry.qry__remote_first}
onchange={handle_search_trigger} onchange={handle_search_trigger}
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
</label> </label>
{/if} {/if}
</span> </span>

View File

@@ -1,98 +1,97 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import { import {
journals_slct, journals_slct,
journals_loc, journals_loc,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { BookType } from '@lucide/svelte'; import { BookType } from '@lucide/svelte';
// Props // Props
let { let {
class: className = '', class: className = '',
placeholder = 'Type your quick note... (First line = Title)', placeholder = 'Type your quick note... (First line = Title)',
journals_li = [] // Optional list of journals to select from journals_li = [] // Optional list of journals to select from
} = $props(); } = $props();
// State // State
let note_content = $state(''); let note_content = $state('');
let is_submitting = $state(false); let is_submitting = $state(false);
// Derived / Local target // Derived / Local target
// We prefer the persisted 'qry__journal_id' if we are on the main landing page // We prefer the persisted 'qry__journal_id' if we are on the main landing page
let selected_journal_id = $state($journals_loc.entry.qry__journal_id); let selected_journal_id = $state($journals_loc.entry.qry__journal_id);
// If a journal is explicitly selected via slct (e.g. we are in a journal view), use that // If a journal is explicitly selected via slct (e.g. we are in a journal view), use that
let target_journal_id = $derived( let target_journal_id = $derived(
$journals_slct.journal_id || selected_journal_id $journals_slct.journal_id || selected_journal_id
); );
async function handle_submit() { async function handle_submit() {
if (!note_content.trim()) return; if (!note_content.trim()) return;
if (!target_journal_id) { if (!target_journal_id) {
alert('Please select a target journal first.'); alert('Please select a target journal first.');
return; return;
} }
is_submitting = true; is_submitting = true;
const lines = note_content.trim().split('\n'); const lines = note_content.trim().split('\n');
let name = lines[0].substring(0, 100); let name = lines[0].substring(0, 100);
if (lines[0].length > 100) name += '...'; if (lines[0].length > 100) name += '...';
// Remove the first line (title) from the content // Remove the first line (title) from the content
const entry_content = lines.slice(1).join('\n').trim(); const entry_content = lines.slice(1).join('\n').trim();
const data_kv = { const data_kv = {
name: name, name: name,
content: entry_content, content: entry_content,
type_code: 'note', type_code: 'note',
private: false, // Ensure notes are public/decrypted by default private: false, // Ensure notes are public/decrypted by default
enable: true, enable: true,
hide: false hide: false
}; };
try { try {
const res = await journals_func.create_ae_obj__journal_entry({ const res = await journals_func.create_ae_obj__journal_entry({
api_cfg: $ae_api, api_cfg: $ae_api,
journal_id: target_journal_id, // Pass string ID here journal_id: target_journal_id, // Pass string ID here
data_kv: data_kv, data_kv: data_kv,
log_lvl: 1 log_lvl: 1
}); });
if (res) { if (res) {
note_content = ''; note_content = '';
// Trigger refresh // Trigger refresh
$journals_trig.journal_entry_li = true; $journals_trig.journal_entry_li = true;
} else { } else {
alert('Failed to create note.');
}
} catch (error) {
console.error('Error creating journal entry:', error);
alert('Failed to create note.'); alert('Failed to create note.');
} }
} catch (error) {
is_submitting = false; console.error('Error creating journal entry:', error);
alert('Failed to create note.');
} }
function handle_keydown(e: KeyboardEvent) { is_submitting = false;
if (e.ctrlKey && e.key === 'Enter') { }
handle_submit();
}
}
function handle_journal_change(e: Event) { function handle_keydown(e: KeyboardEvent) {
const val = (e.target as HTMLSelectElement).value; if (e.ctrlKey && e.key === 'Enter') {
selected_journal_id = val; handle_submit();
$journals_loc.entry.qry__journal_id = val; // Persist choice
} }
}
function handle_journal_change(e: Event) {
const val = (e.target as HTMLSelectElement).value;
selected_journal_id = val;
$journals_loc.entry.qry__journal_id = val; // Persist choice
}
</script> </script>
<div class="card p-4 space-y-4 preset-tonal-surface {className}"> <div class="card preset-tonal-surface space-y-4 p-4 {className}">
<header <header
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2" class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
>
<h3 class="h3 flex items-center gap-2"> <h3 class="h3 flex items-center gap-2">
<BookType size="1.2em" class="text-primary-500" /> <BookType size="1.2em" class="text-primary-500" />
Quick Add Quick Add
@@ -103,11 +102,9 @@
<select <select
class="select select-sm font-bold" class="select select-sm font-bold"
value={target_journal_id} value={target_journal_id}
onchange={handle_journal_change} onchange={handle_journal_change}>
>
<option value="" disabled selected={!target_journal_id} <option value="" disabled selected={!target_journal_id}
>Select Target Journal...</option >Select Target Journal...</option>
>
{#each journals_li as journal (journal.id)} {#each journals_li as journal (journal.id)}
<option value={journal.id}>{journal.name}</option> <option value={journal.id}>{journal.name}</option>
{/each} {/each}
@@ -124,22 +121,19 @@
bind:value={note_content} bind:value={note_content}
{placeholder} {placeholder}
onkeydown={handle_keydown} onkeydown={handle_keydown}
disabled={is_submitting} disabled={is_submitting}></textarea>
></textarea>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span <span
class="text-[10px] opacity-50 font-mono uppercase tracking-tighter hidden sm:block" class="hidden font-mono text-[10px] tracking-tighter uppercase opacity-50 sm:block">
>
Press Ctrl + Enter to save Press Ctrl + Enter to save
</span> </span>
<div class="flex justify-end space-x-2 grow sm:grow-0"> <div class="flex grow justify-end space-x-2 sm:grow-0">
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-surface" class="btn btn-sm preset-tonal-surface"
onclick={() => (note_content = '')} onclick={() => (note_content = '')}
disabled={is_submitting || note_content.length === 0} disabled={is_submitting || note_content.length === 0}>
>
Clear Clear
</button> </button>
<button <button
@@ -148,8 +142,7 @@
onclick={handle_submit} onclick={handle_submit}
disabled={is_submitting || disabled={is_submitting ||
!target_journal_id || !target_journal_id ||
note_content.length === 0} note_content.length === 0}>
>
{#if is_submitting}Saving...{:else}Add Note{/if} {#if is_submitting}Saving...{:else}Add Note{/if}
</button> </button>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,265 +1,265 @@
<script lang="ts"> <script lang="ts">
// *** Import Svelte specific // *** Import Svelte specific
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { BookOpenText, BookPlus, FileDown, FilePlus, FileUp, LoaderCircle, Menu, Pencil, Settings } from '@lucide/svelte'; import {
// *** Import Aether specific variables and functions BookOpenText,
import { ae_util } from '$lib/ae_utils/ae_utils'; BookPlus,
import { FileDown,
ae_snip, FilePlus,
ae_loc, FileUp,
ae_sess, LoaderCircle,
ae_api, Menu,
ae_trig, Pencil,
slct, Settings
slct_trigger } from '@lucide/svelte';
} from '$lib/stores/ae_stores'; // *** Import Aether specific variables and functions
import { import { ae_util } from '$lib/ae_utils/ae_utils';
journals_loc, import {
journals_sess, ae_snip,
journals_slct, ae_loc,
journals_trig, ae_sess,
journals_prom ae_api,
} from '$lib/ae_journals/ae_journals_stores'; ae_trig,
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; slct,
import Journal_obj_id_edit from './ae_comp__journal_obj_id_edit.svelte'; slct_trigger
} from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct,
journals_trig,
journals_prom
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import Journal_obj_id_edit from './ae_comp__journal_obj_id_edit.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_entry_obj_li: any; lq__journal_entry_obj_li: any;
on_show_export?: () => void; on_show_export?: () => void;
on_show_import?: () => void; on_show_import?: () => void;
}
let {
log_lvl = 0,
lq__journal_obj,
lq__journal_entry_obj_li,
on_show_export,
on_show_import
}: Props = $props();
// let ae_promises: key_val = {};
// let ae_tmp: key_val = {};
// let ae_trigger: any = null;
// let ae_triggers: key_val = {};
let typed_journal_passcode: string = $state('');
let passcode_timer: any = $state(null);
$effect(() => {
if (typed_journal_passcode?.length > 4) {
if (!$journals_sess?.journal_kv) {
$journals_sess.journal_kv = {};
}
if (!$journals_sess.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {};
}
verify_journal_passcode();
} }
let { // We need to set a timeout to force the user to re-enter their private passcode
log_lvl = 0, if (
lq__journal_obj, $lq__journal_obj?.id &&
lq__journal_entry_obj_li, $journals_sess?.journal_kv[$lq__journal_obj?.id] &&
on_show_export, $journals_sess?.journal_kv[$lq__journal_obj?.id]
on_show_import ?.journal_passcode_verified
}: Props = $props(); ) {
if (passcode_timer) {
// let ae_promises: key_val = {};
// let ae_tmp: key_val = {};
// let ae_trigger: any = null;
// let ae_triggers: key_val = {};
let typed_journal_passcode: string = $state('');
let passcode_timer: any = $state(null);
$effect(() => {
if (typed_journal_passcode?.length > 4) {
if (!$journals_sess?.journal_kv) {
$journals_sess.journal_kv = {};
}
if (!$journals_sess.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {};
}
verify_journal_passcode();
}
// We need to set a timeout to force the user to re-enter their private passcode
if (
$lq__journal_obj?.id &&
$journals_sess?.journal_kv[$lq__journal_obj?.id] &&
$journals_sess?.journal_kv[$lq__journal_obj?.id]
?.journal_passcode_verified
) {
if (passcode_timer) {
if (log_lvl) {
console.log('Passcode timer already set');
}
return;
}
// Use journal.passcode_timeout (assuming it's in minutes, default to 5)
const timeout_minutes = $lq__journal_obj?.passcode_timeout ?? 5;
const timeout_ms = 1000 * 60 * timeout_minutes;
if (log_lvl) { if (log_lvl) {
console.log( console.log('Passcode timer already set');
`Setting passcode timer for ${timeout_minutes} minutes (${timeout_ms}ms)`
);
} }
return;
passcode_timer = setTimeout(() => {
if (log_lvl) {
console.log('Passcode timer expired');
}
typed_journal_passcode = '';
if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {};
}
// Reset verification and decryption flags
$journals_sess.journal_kv[
$lq__journal_obj?.id
].journal_passcode_verified = false;
$journals_sess.journal_kv[
$lq__journal_obj?.id
].journal_passcode_decrypted = false;
passcode_timer = null;
}, timeout_ms);
} }
});
function verify_journal_passcode() { // Use journal.passcode_timeout (assuming it's in minutes, default to 5)
const timeout_minutes = $lq__journal_obj?.passcode_timeout ?? 5;
const timeout_ms = 1000 * 60 * timeout_minutes;
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`verify_journal_passcode: typed_journal_passcode = ${typed_journal_passcode} journal private passcode = ${$lq__journal_obj?.private_passcode}` `Setting passcode timer for ${timeout_minutes} minutes (${timeout_ms}ms)`
); );
} }
if (typed_journal_passcode === $lq__journal_obj?.private_passcode) { passcode_timer = setTimeout(() => {
console.log('Matched journal private passcode'); if (log_lvl) {
console.log('Passcode timer expired');
}
typed_journal_passcode = '';
if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) { if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {}; $journals_sess.journal_kv[$lq__journal_obj?.id] = {};
} }
$journals_sess.journal_kv[$lq__journal_obj?.id] = {
typed_journal_passcode: typed_journal_passcode,
journal_passcode_verified: true
};
typed_journal_passcode = ''; // Reset verification and decryption flags
} else { $journals_sess.journal_kv[
} $lq__journal_obj?.id
].journal_passcode_verified = false;
$journals_sess.journal_kv[
$lq__journal_obj?.id
].journal_passcode_decrypted = false;
passcode_timer = null;
}, timeout_ms);
}
});
function verify_journal_passcode() {
if (log_lvl) {
console.log(
`verify_journal_passcode: typed_journal_passcode = ${typed_journal_passcode} journal private passcode = ${$lq__journal_obj?.private_passcode}`
);
} }
async function handle_new_entry() { if (typed_journal_passcode === $lq__journal_obj?.private_passcode) {
let data_kv = { console.log('Matched journal private passcode');
category_code: null if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {};
}
$journals_sess.journal_kv[$lq__journal_obj?.id] = {
typed_journal_passcode: typed_journal_passcode,
journal_passcode_verified: true
}; };
if ($journals_loc.entry.qry__category_code) {
data_kv.category_code = $journals_loc.entry.qry__category_code;
}
try { typed_journal_passcode = '';
const results = await journals_func.create_ae_obj__journal_entry({ } else {
api_cfg: $ae_api,
journal_id: $lq__journal_obj?.journal_id,
data_kv: data_kv,
log_lvl: log_lvl
});
if (results?.journal_entry_id) {
$journals_slct.journal_entry_id =
results.journal_entry_id;
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] =
'current';
goto(
`/journals/${$lq__journal_obj?.journal_id}/entry/${results.journal_entry_id}`
);
}
} catch (error) {
console.error('Error creating journal entry:', error);
alert('Failed to create new journal entry.');
}
} }
}
async function handle_new_entry() {
let data_kv = {
category_code: null
};
if ($journals_loc.entry.qry__category_code) {
data_kv.category_code = $journals_loc.entry.qry__category_code;
}
try {
const results = await journals_func.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id: $lq__journal_obj?.journal_id,
data_kv: data_kv,
log_lvl: log_lvl
});
if (results?.journal_entry_id) {
$journals_slct.journal_entry_id = results.journal_entry_id;
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] =
'current';
goto(
`/journals/${$lq__journal_obj?.journal_id}/entry/${results.journal_entry_id}`
);
}
} catch (error) {
console.error('Error creating journal entry:', error);
alert('Failed to create new journal entry.');
}
}
</script> </script>
<div class="relative group/view w-full mx-2 my-1"> <div class="group/view relative mx-2 my-1 w-full">
<!-- Glow ring — mirrors Quick Add and Journal List --> <!-- Glow ring — mirrors Quick Add and Journal List -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 class="from-primary-500 to-secondary-500 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-10 dark:opacity-20 rounded-2xl bg-linear-to-r opacity-10 blur
group-hover/view:opacity-25 dark:group-hover/view:opacity-35 transition duration-700
transition duration-700 pointer-events-none" group-hover/view:opacity-25 dark:opacity-20 dark:group-hover/view:opacity-35">
></div> </div>
<section <section
class="relative rounded-xl p-3 w-full class="relative flex w-full flex-col
flex flex-col gap-2 items-center justify-center items-center justify-center gap-2 rounded-xl border
bg-white dark:bg-gray-900 border-gray-200 bg-white
border border-gray-200 dark:border-gray-700 p-3 text-gray-900 shadow-xl
text-gray-900 dark:text-gray-100 dark:border-gray-700 dark:bg-gray-900
shadow-xl" dark:text-gray-100"
bind:clientHeight={$ae_loc.iframe_height_modal_body} bind:clientHeight={$ae_loc.iframe_height_modal_body}>
> <header
<header class="ae_header journal__header flex w-full flex-row flex-wrap items-center justify-between gap-2">
class="ae_header journal__header flex flex-row flex-wrap gap-2 items-center justify-between w-full" <h2 class="journal__name h3 text-center">
> <BookOpenText class="text-primary-500/80 inline-block" />
<h2 class="journal__name h3 text-center"> {@html $lq__journal_obj?.name ?? 'Loading...'}
<BookOpenText class="inline-block text-primary-500/80" />
{@html $lq__journal_obj?.name ?? 'Loading...'}
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<span <span
class="badge preset-tonal-success font-bold text-lg px-2 ml-2" class="badge preset-tonal-success ml-2 px-2 text-lg font-bold"
title="Entries matching current filters" title="Entries matching current filters">
> {$lq__journal_entry_obj_li?.length ?? '0'}<span
{$lq__journal_entry_obj_li?.length ?? '0'}<span class="ml-0.5 text-xs opacity-50">&times;</span>
class="text-xs opacity-50 ml-0.5">&times;</span </span>
> {/if}
</span>
{/if}
{#await $journals_prom.load__journal_entry_obj_li} {#await $journals_prom.load__journal_entry_obj_li}
<LoaderCircle <LoaderCircle
size="1em" size="1em"
class="inline-block animate-spin ml-1 text-primary-500" class="text-primary-500 ml-1 inline-block animate-spin" />
/> {/await}
{/await} </h2>
</h2>
<div <div
class="grow flex flex-row flex-wrap gap-2 items-center justify-end" class="flex grow flex-row flex-wrap items-center justify-end gap-2">
> <!-- Simplified Config Trigger -->
<!-- Simplified Config Trigger --> <button
<button type="button"
type="button" onclick={() =>
onclick={() => ($journals_sess.show__modal_edit__journal_obj = true)}
($journals_sess.show__modal_edit__journal_obj = true)} class="btn preset-tonal-secondary px-3 py-1 shadow-md"
class="btn preset-tonal-secondary py-1 px-3 shadow-md" title="Journal Config & Actions">
title="Journal Config & Actions" <Settings size="1.2em" class="mr-2" />
> <span class="hidden md:inline">Config</span>
<Settings size="1.2em" class="mr-2" /> </button>
<span class="hidden md:inline">Config</span>
</button>
<!-- Passcode Verification (Condensed) --> <!-- Passcode Verification (Condensed) -->
{#if !$journals_sess?.journal_kv[$lq__journal_obj?.id]?.journal_passcode_verified} {#if !$journals_sess?.journal_kv[$lq__journal_obj?.id]?.journal_passcode_verified}
<div class="flex gap-1"> <div class="flex gap-1">
<input <input
autocomplete="off" autocomplete="off"
type="text" type="text"
bind:value={typed_journal_passcode} bind:value={typed_journal_passcode}
placeholder="Passcode" placeholder="Passcode"
class="input input-sm w-32" class="input input-sm w-32" />
/> </div>
</div> {/if}
{/if} </div>
</div> </header>
</header>
<!-- Show Journal description --> <!-- Show Journal description -->
{#if $lq__journal_obj?.description && $ae_loc.edit_mode} {#if $lq__journal_obj?.description && $ae_loc.edit_mode}
<div <div
class=" class="
prose prose
space-y-1 word-break
p-2 prose-p:m-0
w-full max-w-(--breakpoint-sm) md:max-w-(--breakpoint-md) prose-p:p-0 prose-h1:underline prose-h1:decoration-double
font-mono
bg-gray-50 text-gray-900
dark:bg-gray-800 dark:text-gray-100
shadow-md rounded-lg
text-sm font-normal text-wrap word-break
prose-p:m-0 prose-p:p-0
prose-h1:underline prose-h1:decoration-double
prose-h2:underline prose-h2:underline
prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-h1:text-2xl prose-h2:text-xl
prose-h1:m-0 prose-h2:m-0 prose-h3:m-0 prose-h4:m-0 prose-h5:m-0 prose-h6:m-0 prose-h3:text-lg prose-h1:m-0
prose-h2:m-0 prose-h3:m-0
prose-h4:m-0 prose-h5:m-0 prose-h6:m-0 prose-li:m-0
prose-li:m-0 prose-li:p-0 prose-li:line-height-none prose-li:p-0 prose-li:line-height-none
" w-full max-w-(--breakpoint-sm)
> space-y-1
{@html $lq__journal_obj.description_md_html} rounded-lg bg-gray-50 p-2
</div> font-mono text-sm font-normal text-wrap text-gray-900 shadow-md
{/if}
md:max-w-(--breakpoint-md) dark:bg-gray-800 dark:text-gray-100
">
{@html $lq__journal_obj.description_md_html}
</div>
{/if}
</section> </section>
</div> </div>
@@ -270,5 +270,4 @@
bind:show={$journals_sess.show__modal_edit__journal_obj} bind:show={$journals_sess.show__modal_edit__journal_obj}
on_new_entry={handle_new_entry} on_new_entry={handle_new_entry}
{on_show_export} {on_show_export}
{on_show_import} {on_show_import} />
/>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_obj_li.svelte * ae_comp__journal_obj_li.svelte
* Modernized Journal List Component * Modernized Journal List Component
* Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop) * Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop)
* Style: Tailwind 4 + Skeleton UI Reference Standard * Style: Tailwind 4 + Skeleton UI Reference Standard
*/ */
import { BookOpenText, BookType, Calendar, Clock, Hash } from '@lucide/svelte'; import { BookOpenText, BookType, Calendar, Clock, Hash } from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
interface Props { interface Props {
lq__journal_obj_li: any; lq__journal_obj_li: any;
} }
let { lq__journal_obj_li }: Props = $props(); let { lq__journal_obj_li }: Props = $props();
</script> </script>
<!-- <!--
@@ -22,109 +22,138 @@
Uses plain Tailwind gray scale with explicit dark: variants — no Skeleton paired utilities — Uses plain Tailwind gray scale with explicit dark: variants — no Skeleton paired utilities —
so behavior is predictable regardless of which Skeleton theme is active. so behavior is predictable regardless of which Skeleton theme is active.
--> -->
<div class="w-full max-w-2xl relative group/list"> <div class="group/list relative w-full max-w-2xl">
<!-- Glow ring behind the list, same technique as Quick Add --> <!-- Glow ring behind the list, same technique as Quick Add -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 class="from-primary-500 to-secondary-500 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-10 dark:opacity-20 rounded-2xl bg-linear-to-r opacity-10 blur
group-hover/list:opacity-25 dark:group-hover/list:opacity-35 transition duration-700
transition duration-700 pointer-events-none" group-hover/list:opacity-25 dark:opacity-20 dark:group-hover/list:opacity-35">
></div> </div>
<section class="journal_list relative w-full space-y-1.5 p-2 sm:p-3 <section
bg-white dark:bg-gray-900 class="journal_list relative w-full space-y-1.5 rounded-2xl border
border border-gray-200 dark:border-gray-700 border-gray-200 bg-white
rounded-2xl shadow-xl"> p-2 shadow-xl sm:p-3
dark:border-gray-700 dark:bg-gray-900">
{#if $lq__journal_obj_li && $lq__journal_obj_li.length} {#if $lq__journal_obj_li && $lq__journal_obj_li.length}
{#each $lq__journal_obj_li as journal (journal.journal_id)} {#each $lq__journal_obj_li as journal (journal.journal_id)}
<a <a
href="/journals/{journal?.journal_id}" href="/journals/{journal?.journal_id}"
class="journal_card group relative class="journal_card group border-l-primary-500/60
flex items-center gap-3 px-4 py-3 hover:border-l-primary-500 relative flex items-center gap-3
bg-gray-50 dark:bg-gray-800 rounded-xl border
border border-gray-200 dark:border-gray-700 border-l-4 border-gray-200 bg-gray-50
border-l-4 border-l-primary-500/60 px-4 py-3
rounded-xl shadow-sm shadow-sm transition-all
hover:shadow-md hover:border-l-primary-500 duration-150 ease-in-out
hover:bg-gray-100 dark:hover:bg-gray-700 hover:bg-gray-100 hover:shadow-md
active:scale-[0.99] active:scale-[0.99]
transition-all duration-150 ease-in-out" dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
class:hidden={(journal?.hide || !journal?.enable) && !$ae_loc.trusted_access} class:hidden={(journal?.hide || !journal?.enable) &&
!$ae_loc.trusted_access}
class:opacity-60={journal.hide} class:opacity-60={journal.hide}
class:!border-l-warning-500={!journal?.enable} class:!border-l-warning-500={!journal?.enable}>
>
<!-- Icon: fixed size, never squashed --> <!-- Icon: fixed size, never squashed -->
<div class="shrink-0 p-2 bg-primary-500/10 rounded-lg text-primary-500 <div
group-hover:bg-primary-500 group-hover:text-white transition-colors"> class="bg-primary-500/10 text-primary-500 group-hover:bg-primary-500 shrink-0 rounded-lg
p-2 transition-colors group-hover:text-white">
<BookType size="1.3em" /> <BookType size="1.3em" />
</div> </div>
<!-- Name + badge: min-w-0 flex-1 lets text shrink and wrap --> <!-- Name + badge: min-w-0 flex-1 lets text shrink and wrap -->
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="text-sm sm:text-base font-bold <div
text-gray-900 dark:text-gray-100 class="text-sm leading-snug font-bold
leading-snug break-words"> break-words text-gray-900
sm:text-base dark:text-gray-100">
{journal.name} {journal.name}
</div> </div>
<div class="flex flex-wrap items-center gap-2 mt-0.5"> <div class="mt-0.5 flex flex-wrap items-center gap-2">
{#if journal.type_code} {#if journal.type_code}
<span class="badge preset-tonal-warning text-[10px] uppercase tracking-wider font-bold"> <span
class="badge preset-tonal-warning text-[10px] font-bold tracking-wider uppercase">
{journal.type_code} {journal.type_code}
</span> </span>
{/if} {/if}
<!-- Description snippet: edit mode only --> <!-- Description snippet: edit mode only -->
{#if journal.description && $ae_loc.edit_mode} {#if journal.description && $ae_loc.edit_mode}
<span class="text-[11px] text-gray-500 dark:text-gray-400 font-mono truncate max-w-[16rem]"> <span
{ae_util.strip_html ? ae_util.strip_html(journal.description).slice(0, 60) : journal.description.slice(0, 60)}&hellip; class="max-w-[16rem] truncate font-mono text-[11px] text-gray-500 dark:text-gray-400">
{ae_util.strip_html
? ae_util
.strip_html(journal.description)
.slice(0, 60)
: journal.description.slice(
0,
60
)}&hellip;
</span> </span>
{/if} {/if}
</div> </div>
</div> </div>
<!-- Stats: right-aligned, compact --> <!-- Stats: right-aligned, compact -->
<div class="shrink-0 flex flex-col items-end gap-0.5 <div
class="flex shrink-0 flex-col items-end gap-0.5
text-xs text-gray-500 dark:text-gray-400"> text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-1" title="Entry Count"> <div
class="flex items-center gap-1"
title="Entry Count">
<Hash size="0.85em" /> <Hash size="0.85em" />
<span class="font-bold tabular-nums">{journal.journal_entry_count ?? 0}&times;</span> <span class="font-bold tabular-nums"
>{journal.journal_entry_count ??
0}&times;</span>
</div> </div>
<div class="flex items-center gap-1" title="Last Updated"> <div
class="flex items-center gap-1"
title="Last Updated">
<Clock size="0.85em" /> <Clock size="0.85em" />
<span class="tabular-nums">{ae_util.iso_datetime_formatter( <span class="tabular-nums"
journal.updated_on || journal.created_on, >{ae_util.iso_datetime_formatter(
'date_short' journal.updated_on || journal.created_on,
)}</span>
</div>
{#if $ae_loc.edit_mode && $ae_loc.administrator_access}
<div class="flex items-center gap-1 opacity-40" title="Created">
<Calendar size="0.85em" />
<span class="font-mono text-[10px] tabular-nums">{ae_util.iso_datetime_formatter(
journal.created_on,
'date_short' 'date_short'
)}</span> )}</span>
</div>
{#if $ae_loc.edit_mode && $ae_loc.administrator_access}
<div
class="flex items-center gap-1 opacity-40"
title="Created">
<Calendar size="0.85em" />
<span class="font-mono text-[10px] tabular-nums"
>{ae_util.iso_datetime_formatter(
journal.created_on,
'date_short'
)}</span>
</div> </div>
{/if} {/if}
</div> </div>
<!-- Chevron hint: desktop only --> <!-- Chevron hint: desktop only -->
<BookOpenText size="0.95em" <BookOpenText
class="shrink-0 opacity-0 group-hover:opacity-30 transition-opacity hidden sm:block" /> size="0.95em"
class="hidden shrink-0 opacity-0 transition-opacity group-hover:opacity-30 sm:block" />
<!-- Status overlays: edit mode only --> <!-- Status overlays: edit mode only -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="absolute -top-1.5 -right-1.5 flex gap-1"> <div class="absolute -top-1.5 -right-1.5 flex gap-1">
{#if journal.hide} {#if journal.hide}
<span class="badge-icon preset-tonal-surface shadow-sm" title="Hidden">🚫</span> <span
class="badge-icon preset-tonal-surface shadow-sm"
title="Hidden">🚫</span>
{/if} {/if}
{#if !journal.enable} {#if !journal.enable}
<span class="badge-icon preset-tonal-warning shadow-sm" title="Disabled">⚠️</span> <span
class="badge-icon preset-tonal-warning shadow-sm"
title="Disabled">⚠️</span>
{/if} {/if}
</div> </div>
{/if} {/if}
</a> </a>
{/each} {/each}
{:else} {:else}
<div class="p-20 text-center text-gray-400 dark:text-gray-500 flex flex-col items-center gap-4"> <div
class="flex flex-col items-center gap-4 p-20 text-center text-gray-400 dark:text-gray-500">
<BookType size="4em" /> <BookType size="4em" />
<p class="text-xl">No journals found in this view.</p> <p class="text-xl">No journals found in this view.</p>
</div> </div>

View File

@@ -1,50 +1,63 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
/** /**
* ae_comp__modal_journal_config.svelte * ae_comp__modal_journal_config.svelte
* Standardized Module-level settings for Journals. * Standardized Module-level settings for Journals.
* Fixed Svelte 5 state placement and reactivity loops. * Fixed Svelte 5 state placement and reactivity loops.
*/ */
import { CalendarClock, Check, CodeXml, Database, Layout, MonitorPlay, MousePointerClick, Palette, Settings, ShieldCheck, Wrench, X } from '@lucide/svelte'; import {
import { Modal } from 'flowbite-svelte'; CalendarClock,
Check,
CodeXml,
Database,
Layout,
MonitorPlay,
MousePointerClick,
Palette,
Settings,
ShieldCheck,
Wrench,
X
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
show?: boolean; show?: boolean;
}
let { log_lvl = 0, show = $bindable(false) }: Props = $props();
// Internal State
let tab: 'form' | 'local_json' | 'session_json' = $state('form');
let tmp_config: any = $state({
journal: {},
entry: {}
});
$effect(() => {
if (show) {
untrack(() => {
const fresh_config = JSON.parse(JSON.stringify($journals_loc));
// Ensure the structure is preserved
if (!fresh_config.entry) fresh_config.entry = {};
tmp_config = fresh_config;
});
} }
});
let { log_lvl = 0, show = $bindable(false) }: Props = $props(); function handle_save() {
journals_loc.set(tmp_config);
// Internal State show = false;
let tab: 'form' | 'local_json' | 'session_json' = $state('form'); }
let tmp_config: any = $state({
journal: {},
entry: {}
});
$effect(() => {
if (show) {
untrack(() => {
const fresh_config = JSON.parse(JSON.stringify($journals_loc));
// Ensure the structure is preserved
if (!fresh_config.entry) fresh_config.entry = {};
tmp_config = fresh_config;
});
}
});
function handle_save() {
journals_loc.set(tmp_config);
show = false;
}
</script> </script>
<Modal <Modal
@@ -53,32 +66,32 @@
dismissable={false} dismissable={false}
placement="top-center" placement="top-center"
size="xl" size="xl"
class="relative flex flex-col mx-auto w-full bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-orange-300 dark:border-orange-700 rounded-lg shadow-xl" class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800" headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800" footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
>
{#snippet header()} {#snippet header()}
<h3 class="flex-1 flex items-center gap-2 text-lg font-bold"> <h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Wrench class="text-primary-500" /> <Wrench class="text-primary-500" />
<span>&AElig; Journals Module Config</span> <span>&AElig; Journals Module Config</span>
</h3> </h3>
<button type="button" class="btn-icon btn-icon-sm preset-tonal-surface ml-2" onclick={() => (show = false)}> <button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" /> <X size="1.1em" />
</button> </button>
{/snippet} {/snippet}
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4"> <div class="h-[60vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div <div
class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto sticky top-0 z-10 backdrop-blur-sm" class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
>
<button <button
type="button" type="button"
class="btn btn-sm transition-all {tab === 'form' class="btn btn-sm transition-all {tab === 'form'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'form')} onclick={() => (tab = 'form')}>
>
<Settings size="1.1em" class="mr-1" /> Config <Settings size="1.1em" class="mr-1" /> Config
</button> </button>
<button <button
@@ -86,8 +99,7 @@
class="btn btn-sm transition-all {tab === 'local_json' class="btn btn-sm transition-all {tab === 'local_json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'local_json')} onclick={() => (tab = 'local_json')}>
>
<Database size="1.1em" class="mr-1" /> Local JSON <Database size="1.1em" class="mr-1" /> Local JSON
</button> </button>
<button <button
@@ -95,71 +107,55 @@
class="btn btn-sm transition-all {tab === 'session_json' class="btn btn-sm transition-all {tab === 'session_json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'session_json')} onclick={() => (tab = 'session_json')}>
>
<CodeXml size="1.1em" class="mr-1" /> Session JSON <CodeXml size="1.1em" class="mr-1" /> Session JSON
</button> </button>
</div> </div>
{#if tab === 'form'} {#if tab === 'form'}
<div class="space-y-8 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-8 duration-300">
<!-- Date/Time Section --> <!-- Date/Time Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<CalendarClock size="1.2em" class="text-primary-500" /> <CalendarClock size="1.2em" class="text-primary-500" />
Date and Time Display Date and Time Display
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>DateTime Format</span >DateTime Format</span>
>
<select <select
bind:value={tmp_config.datetime_format} bind:value={tmp_config.datetime_format}
class="select select-sm" class="select select-sm">
>
<option value="datetime_12_short" <option value="datetime_12_short"
>MMM D, YY hh:mm A</option >MMM D, YY hh:mm A</option>
>
<option value="datetime_12_long" <option value="datetime_12_long"
>MMMM D, YYYY hh:mm A</option >MMMM D, YYYY hh:mm A</option>
>
<option value="datetime_short" <option value="datetime_short"
>MMM D, YY HH:mm</option >MMM D, YY HH:mm</option>
>
<option value="datetime_long" <option value="datetime_long"
>MMMM D, YYYY HH:mm</option >MMMM D, YYYY HH:mm</option>
>
<option value="datetime_us" <option value="datetime_us"
>US (MM/DD/YYYY hh:mm:ss A)</option >US (MM/DD/YYYY hh:mm:ss A)</option>
>
<option value="datetime_iso" <option value="datetime_iso"
>ISO (YYYY-MM-DD HH:mm:ss)</option >ISO (YYYY-MM-DD HH:mm:ss)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Time-Only Format</span >Time-Only Format</span>
>
<select <select
bind:value={tmp_config.time_format} bind:value={tmp_config.time_format}
class="select select-sm" class="select select-sm">
>
<option value="time_12_short" <option value="time_12_short"
>12-hour short (3:30 PM)</option >12-hour short (3:30 PM)</option>
>
<option value="time_12_long" <option value="time_12_long"
>12-hour long (3:30:45 PM)</option >12-hour long (3:30:45 PM)</option>
>
<option value="time_short" <option value="time_short"
>24-hour short (15:30)</option >24-hour short (15:30)</option>
>
<option value="time_long" <option value="time_long"
>24-hour long (15:30:45)</option >24-hour long (15:30:45)</option>
>
</select> </select>
</label> </label>
</div> </div>
@@ -168,23 +164,19 @@
<!-- UI Section --> <!-- UI Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<MousePointerClick <MousePointerClick
size="1.2em" size="1.2em"
class="text-primary-500" class="text-primary-500" />
/>
User Interface Preferences User Interface Preferences
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label <label
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_config.entry.auto_save} bind:checked={tmp_config.entry.auto_save}
class="checkbox" class="checkbox" />
/>
<div class="space-y-0.5"> <div class="space-y-0.5">
<span class="font-bold">Enable Auto-Save</span> <span class="font-bold">Enable Auto-Save</span>
<p class="text-xs opacity-60"> <p class="text-xs opacity-60">
@@ -193,16 +185,14 @@
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_config.show_id_random} bind:checked={tmp_config.show_id_random}
class="checkbox" class="checkbox" />
/>
<div class="space-y-0.5"> <div class="space-y-0.5">
<span class="font-bold">Show Technical IDs</span <span class="font-bold"
> >Show Technical IDs</span>
<p class="text-xs opacity-60"> <p class="text-xs opacity-60">
Display UUIDs in metadata footers Display UUIDs in metadata footers
</p> </p>
@@ -214,52 +204,42 @@
<!-- Journal Query Filters Section --> <!-- Journal Query Filters Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<Database size="1.2em" class="text-primary-500" /> <Database size="1.2em" class="text-primary-500" />
Journal Query Filters Journal Query Filters
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Enabled Status</span >Enabled Status</span>
>
<select <select
bind:value={tmp_config.journal.qry__enabled} bind:value={tmp_config.journal.qry__enabled}
class="select select-sm" class="select select-sm">
>
<option value="enabled">Enabled Only</option> <option value="enabled">Enabled Only</option>
<option value="not_enabled" <option value="not_enabled"
>Disabled Only</option >Disabled Only</option>
>
<option value="all" <option value="all"
>All (Enabled & Disabled & NULL)</option >All (Enabled & Disabled & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Hidden Status</span >Hidden Status</span>
>
<select <select
bind:value={tmp_config.journal.qry__hidden} bind:value={tmp_config.journal.qry__hidden}
class="select select-sm" class="select select-sm">
>
<option value="not_hidden">Visible Only</option> <option value="not_hidden">Visible Only</option>
<option value="hidden">Hidden Only</option> <option value="hidden">Hidden Only</option>
<option value="all" <option value="all"
>All (Visible & Hidden & NULL)</option >All (Visible & Hidden & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Query Limit</span >Query Limit</span>
>
<select <select
bind:value={tmp_config.journal.qry__limit} bind:value={tmp_config.journal.qry__limit}
class="select select-sm" class="select select-sm">
>
<option value={10}>10</option> <option value={10}>10</option>
<option value={20}>20</option> <option value={20}>20</option>
<option value={50}>50</option> <option value={50}>50</option>
@@ -275,52 +255,42 @@
<!-- Entry Query Filters Section --> <!-- Entry Query Filters Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<Database size="1.2em" class="text-primary-500" /> <Database size="1.2em" class="text-primary-500" />
Entry Query Filters Entry Query Filters
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Enabled Status</span >Enabled Status</span>
>
<select <select
bind:value={tmp_config.entry.qry__enabled} bind:value={tmp_config.entry.qry__enabled}
class="select select-sm" class="select select-sm">
>
<option value="enabled">Enabled Only</option> <option value="enabled">Enabled Only</option>
<option value="not_enabled" <option value="not_enabled"
>Disabled Only</option >Disabled Only</option>
>
<option value="all" <option value="all"
>All (Enabled & Disabled & NULL)</option >All (Enabled & Disabled & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Hidden Status</span >Hidden Status</span>
>
<select <select
bind:value={tmp_config.entry.qry__hidden} bind:value={tmp_config.entry.qry__hidden}
class="select select-sm" class="select select-sm">
>
<option value="not_hidden">Visible Only</option> <option value="not_hidden">Visible Only</option>
<option value="hidden">Hidden Only</option> <option value="hidden">Hidden Only</option>
<option value="all" <option value="all"
>All (Visible & Hidden & NULL)</option >All (Visible & Hidden & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Query Limit</span >Query Limit</span>
>
<select <select
bind:value={tmp_config.entry.qry__limit} bind:value={tmp_config.entry.qry__limit}
class="select select-sm" class="select select-sm">
>
<option value={10}>10</option> <option value={10}>10</option>
<option value={20}>20</option> <option value={20}>20</option>
<option value={50}>50</option> <option value={50}>50</option>
@@ -336,30 +306,25 @@
<!-- Security Section --> <!-- Security Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<ShieldCheck size="1.2em" class="text-primary-500" /> <ShieldCheck size="1.2em" class="text-primary-500" />
Security & Encryption Security & Encryption
</h2> </h2>
<div <div
class="p-4 bg-orange-500/5 rounded-lg border border-orange-500/20 space-y-4" class="space-y-4 rounded-lg border border-orange-500/20 bg-orange-500/5 p-4">
> <div class="text-sm italic opacity-80">
<div class="text-sm opacity-80 italic">
Global security overrides for the journal module. Global security overrides for the journal module.
</div> </div>
<label <label
class="flex items-center space-x-3 cursor-pointer" class="flex cursor-pointer items-center space-x-3">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$journals_sess.enable_session_passcode_cache $journals_sess.enable_session_passcode_cache
} }
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<span class="text-sm font-bold" <span class="text-sm font-bold"
>Cache Passcodes in Session</span >Cache Passcodes in Session</span>
>
</label> </label>
</div> </div>
</section> </section>
@@ -370,8 +335,7 @@
readonly={true} readonly={true}
content={JSON.stringify(tmp_config, null, 2)} content={JSON.stringify(tmp_config, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{:else if tab === 'session_json'} {:else if tab === 'session_json'}
<div class="h-full min-h-[400px]"> <div class="h-full min-h-[400px]">
@@ -379,8 +343,7 @@
readonly={true} readonly={true}
content={JSON.stringify($journals_sess, null, 2)} content={JSON.stringify($journals_sess, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -389,16 +352,14 @@
<div class="flex gap-4"> <div class="flex gap-4">
<button <button
type="button" type="button"
class="btn preset-tonal-surface font-bold min-w-[100px]" class="btn preset-tonal-surface min-w-[100px] font-bold"
onclick={() => (show = false)} onclick={() => (show = false)}>
>
<X size="1.2em" class="mr-2" /> Cancel <X size="1.2em" class="mr-2" /> Cancel
</button> </button>
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold shadow-lg min-w-[120px]" class="btn preset-filled-primary min-w-[120px] font-bold shadow-lg"
onclick={handle_save} onclick={handle_save}>
>
<Check size="1.2em" class="mr-2" /> Save Changes <Check size="1.2em" class="mr-2" /> Save Changes
</button> </button>
</div> </div>

View File

@@ -1,138 +1,135 @@
<script lang="ts"> <script lang="ts">
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Check, X } from '@lucide/svelte'; import { Check, X } from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
interface Props { interface Props {
open: boolean; open: boolean;
journal_entry: key_val; journal_entry: key_val;
journal_config: key_val; // The cfg_json from the journal object journal_config: key_val; // The cfg_json from the journal object
mode?: 'append' | 'prepend' | 'auto'; mode?: 'append' | 'prepend' | 'auto';
on_close: () => void; on_close: () => void;
on_update: () => void; on_update: () => void;
log_lvl?: number; log_lvl?: number;
}
let {
open = $bindable(false),
journal_entry,
journal_config,
mode = 'auto',
on_close,
on_update,
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';
} }
let { // Determine Append or Prepend
open = $bindable(false), let effective_mode = mode;
journal_entry, if (effective_mode === 'auto') {
journal_config, effective_mode = journal_config?.entry_add_text || 'append';
mode = 'auto', }
on_close,
on_update,
log_lvl = 0
}: Props = $props();
// Local State
let tmp_entry_obj: key_val = $state({});
// Header Options if (effective_mode == 'prepend') {
let add_timestamp_header: boolean = $state(true); new_content = add_content + new_content;
let add_timestamp_header_w_day_of_week: boolean = $state(true); } else {
let add_text_header: string = $state(''); // Append
let add_text: string = $state(''); new_content = new_content.trim() + '\n\n' + add_content;
}
// Change detection new_content = new_content.trim() + '\n';
let has_changes: boolean = $derived(
add_text_header.length > 0 || add_text.length > 0
);
// Initialize tmp object when entry changes or modal opens let data_kv = { content: new_content };
$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() { try {
let current_entry_content = tmp_entry_obj?.content || ''; let update_result = await journals_func.update_ae_obj__journal_entry({
let add_content = ''; api_cfg: $ae_api,
let new_content = current_entry_content; journal_entry_id: tmp_entry_obj?.journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
});
// Construct the header/content to add (Following original logic) if (update_result) {
let timestamp_str = ae_util.iso_datetime_formatter( // Success
new Date(), on_update();
'datetime_iso_12_no_seconds' open = false;
);
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 { } 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
on_update();
open = false;
} else {
alert('Failed to update journal entry.');
}
} catch (error) {
console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.'); alert('Failed to update journal entry.');
} }
} catch (error) {
console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.');
} }
}
</script> </script>
<Modal <Modal
@@ -145,8 +142,7 @@
autoclose={false} autoclose={false}
placement="top-center" placement="top-center"
size="xl" 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" class="top-center relative mx-auto flex w-full flex-col gap-1 divide-gray-200 rounded-lg border-gray-200 bg-white text-gray-800 shadow-md dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200">
>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<!-- Checkbox Options --> <!-- Checkbox Options -->
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
@@ -155,8 +151,7 @@
type="checkbox" type="checkbox"
id="append_timestamp_header" id="append_timestamp_header"
bind:checked={add_timestamp_header} bind:checked={add_timestamp_header}
class="checkbox" class="checkbox" />
/>
<span>Use timestamp as Markdown header</span> <span>Use timestamp as Markdown header</span>
</label> </label>
@@ -165,8 +160,7 @@
type="checkbox" type="checkbox"
id="append_timestamp_header_w_day_of_week" id="append_timestamp_header_w_day_of_week"
bind:checked={add_timestamp_header_w_day_of_week} bind:checked={add_timestamp_header_w_day_of_week}
class="checkbox" class="checkbox" />
/>
<span>Include day of week</span> <span>Include day of week</span>
</label> </label>
</div> </div>
@@ -176,8 +170,7 @@
type="text" type="text"
placeholder="Markdown header for content (Optional)" placeholder="Markdown header for content (Optional)"
bind:value={add_text_header} bind:value={add_text_header}
class="input" class="input" />
/>
<!-- Main Content Area --> <!-- Main Content Area -->
<textarea <textarea
@@ -185,26 +178,23 @@
class="textarea min-h-48" class="textarea min-h-48"
placeholder="Content to {mode === 'auto' placeholder="Content to {mode === 'auto'
? journal_config?.entry_add_text ? journal_config?.entry_add_text
: mode}..." : mode}...">
>
</textarea> </textarea>
<div class="flex justify-end gap-2 mt-2"> <div class="mt-2 flex justify-end gap-2">
<button <button
type="button" type="button"
disabled={!has_changes} disabled={!has_changes}
onclick={handle_save} onclick={handle_save}
class="btn preset-filled-primary" class="btn preset-filled-primary"
class:opacity-50={!has_changes} class:opacity-50={!has_changes}>
>
<Check class="mr-1" /> <Check class="mr-1" />
Update Update
</button> </button>
<button <button
type="button" type="button"
onclick={on_close} onclick={on_close}
class="btn preset-tonal-surface" class="btn preset-tonal-surface">
>
<X class="mr-1" /> <X class="mr-1" />
Cancel Cancel
</button> </button>

View File

@@ -1,89 +1,108 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__modal_journal_entry_config.svelte * ae_comp__modal_journal_entry_config.svelte
* Standardized Journal Entry-level configuration. * Standardized Journal Entry-level configuration.
*/ */
import { ArrowDownToLine, ArrowUpToLine, Check, Clock, CodeXml, Copy, Database, FileDown, Fingerprint, Minus, Plus, RefreshCcw, Settings, Shapes, ShieldCheck, Tag, Trash2, X, Zap } from '@lucide/svelte'; import {
import { Modal } from 'flowbite-svelte'; ArrowDownToLine,
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; ArrowUpToLine,
import { Check,
journals_loc, Clock,
journals_sess CodeXml,
} from '$lib/ae_journals/ae_journals_stores'; Copy,
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; Database,
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; FileDown,
import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte'; Fingerprint,
Minus,
Plus,
RefreshCcw,
Settings,
Shapes,
ShieldCheck,
Tag,
Trash2,
X,
Zap
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
show?: boolean; show?: boolean;
entry: any; entry: any;
journal: any; journal: any;
tmp_entry_obj: any; // Bindable tmp_entry_obj: any; // Bindable
on_save: () => void; on_save: () => void;
on_force_reset?: () => void; on_force_reset?: () => void;
on_show_export?: () => void; on_show_export?: () => void;
on_append?: () => void; on_append?: () => void;
on_prepend?: () => void; on_prepend?: () => void;
} }
let { let {
log_lvl = $bindable(0), log_lvl = $bindable(0),
show = $bindable(false), show = $bindable(false),
entry, entry,
journal, journal,
tmp_entry_obj = $bindable(), tmp_entry_obj = $bindable(),
on_save, on_save,
on_force_reset, on_force_reset,
on_show_export, on_show_export,
on_append, on_append,
on_prepend on_prepend
}: Props = $props(); }: Props = $props();
let tab: 'actions' | 'meta' | 'security' | 'json' = $state('actions'); let tab: 'actions' | 'meta' | 'security' | 'json' = $state('actions');
const normalize_date = (val: string | null) => const normalize_date = (val: string | null) => (val ? val.slice(0, 16) : null);
val ? val.slice(0, 16) : null;
async function handle_update_entry() {
async function handle_update_entry() { try {
try { // WHITELISTED BASE TABLE COLUMNS ONLY
// WHITELISTED BASE TABLE COLUMNS ONLY const data_kv = {
const data_kv = { name: tmp_entry_obj.name,
name: tmp_entry_obj.name, short_name: tmp_entry_obj.short_name,
short_name: tmp_entry_obj.short_name, summary: tmp_entry_obj.summary,
summary: tmp_entry_obj.summary, content: tmp_entry_obj.content,
content: tmp_entry_obj.content, category_code: tmp_entry_obj.category_code,
category_code: tmp_entry_obj.category_code, type_code: tmp_entry_obj.type_code,
type_code: tmp_entry_obj.type_code, topic_code: tmp_entry_obj.topic_code,
topic_code: tmp_entry_obj.topic_code, tags: tmp_entry_obj.tags,
tags: tmp_entry_obj.tags, private: tmp_entry_obj.private,
private: tmp_entry_obj.private, public: tmp_entry_obj.public,
public: tmp_entry_obj.public, personal: tmp_entry_obj.personal,
personal: tmp_entry_obj.personal, professional: tmp_entry_obj.professional,
professional: tmp_entry_obj.professional, alert: tmp_entry_obj.alert,
alert: tmp_entry_obj.alert, alert_msg: tmp_entry_obj.alert_msg,
alert_msg: tmp_entry_obj.alert_msg, enable: tmp_entry_obj.enable,
enable: tmp_entry_obj.enable, hide: tmp_entry_obj.hide,
hide: tmp_entry_obj.hide, priority: tmp_entry_obj.priority,
priority: tmp_entry_obj.priority, sort: tmp_entry_obj.sort,
sort: tmp_entry_obj.sort, group: tmp_entry_obj.group,
group: tmp_entry_obj.group, notes: tmp_entry_obj.notes,
notes: tmp_entry_obj.notes, archive_on: tmp_entry_obj.archive_on,
archive_on: tmp_entry_obj.archive_on, template: tmp_entry_obj.template,
template: tmp_entry_obj.template, data_json: tmp_entry_obj.data_json
data_json: tmp_entry_obj.data_json };
};
await journals_func.update_ae_obj__journal_entry({
await journals_func.update_ae_obj__journal_entry({ api_cfg: $ae_api,
api_cfg: $ae_api, journal_entry_id: tmp_entry_obj.journal_entry_id,
journal_entry_id: tmp_entry_obj.journal_entry_id, data_kv: data_kv,
data_kv: data_kv, log_lvl: log_lvl
log_lvl: log_lvl });
}); } catch (error) {
} catch (error) { console.error('Error updating journal entry:', error);
console.error('Error updating journal entry:', error);
}
} }
}
</script> </script>
<Modal <Modal
@@ -92,32 +111,32 @@
dismissable={false} dismissable={false}
placement="top-center" placement="top-center"
size="lg" size="lg"
class="relative flex flex-col mx-auto w-full bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-orange-300 dark:border-orange-700 rounded-lg shadow-xl" class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800" headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800" footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
>
{#snippet header()} {#snippet header()}
<h3 class="flex-1 flex items-center gap-2 text-lg font-bold"> <h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Settings class="text-primary-500" /> <Settings class="text-primary-500" />
<span>Entry Config: {tmp_entry_obj.name || '--'}</span> <span>Entry Config: {tmp_entry_obj.name || '--'}</span>
</h3> </h3>
<button type="button" class="btn-icon btn-icon-sm preset-tonal-surface ml-2" onclick={() => (show = false)}> <button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" /> <X size="1.1em" />
</button> </button>
{/snippet} {/snippet}
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4"> <div class="h-[60vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div <div
class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto sticky top-0 z-10 backdrop-blur-sm" class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
>
<button <button
type="button" type="button"
class="btn btn-sm transition-all {tab === 'actions' class="btn btn-sm transition-all {tab === 'actions'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'actions')} onclick={() => (tab = 'actions')}>
>
<Zap size="1.1em" class="mr-1" /> Quick Actions <Zap size="1.1em" class="mr-1" /> Quick Actions
</button> </button>
<button <button
@@ -125,8 +144,7 @@
class="btn btn-sm transition-all {tab === 'meta' class="btn btn-sm transition-all {tab === 'meta'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'meta')} onclick={() => (tab = 'meta')}>
>
<Shapes size="1.1em" class="mr-1" /> Metadata <Shapes size="1.1em" class="mr-1" /> Metadata
</button> </button>
<button <button
@@ -134,8 +152,7 @@
class="btn btn-sm transition-all {tab === 'security' class="btn btn-sm transition-all {tab === 'security'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'security')} onclick={() => (tab = 'security')}>
>
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security <ShieldCheck size="1.1em" class="mr-1" /> Status & Security
</button> </button>
<button <button
@@ -143,23 +160,21 @@
class="btn btn-sm transition-all {tab === 'json' class="btn btn-sm transition-all {tab === 'json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'json')} onclick={() => (tab = 'json')}>
>
<CodeXml size="1.1em" class="mr-1" /> JSON <CodeXml size="1.1em" class="mr-1" /> JSON
</button> </button>
</div> </div>
{#if tab === 'actions'} {#if tab === 'actions'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<section class="grid grid-cols-1 md:grid-cols-2 gap-4"> <section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<button <button
type="button" type="button"
class="btn preset-tonal-secondary w-full" class="btn preset-tonal-secondary w-full"
onclick={() => { onclick={() => {
show = false; show = false;
on_prepend?.(); on_prepend?.();
}} }}>
>
<ArrowUpToLine size="1.2em" class="mr-2" /> Prepend Content <ArrowUpToLine size="1.2em" class="mr-2" /> Prepend Content
</button> </button>
<button <button
@@ -168,8 +183,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_append?.(); on_append?.();
}} }}>
>
<ArrowDownToLine size="1.2em" class="mr-2" /> Append Content <ArrowDownToLine size="1.2em" class="mr-2" /> Append Content
</button> </button>
<button <button
@@ -178,8 +192,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_show_export?.(); on_show_export?.();
}} }}>
>
<FileDown size="1.2em" class="mr-2" /> Export Entry <FileDown size="1.2em" class="mr-2" /> Export Entry
</button> </button>
<button <button
@@ -189,13 +202,12 @@
/* Clone logic here */ alert( /* Clone logic here */ alert(
'Clone not yet implemented in modal' 'Clone not yet implemented in modal'
); );
}} }}>
>
<Copy size="1.2em" class="mr-2" /> Clone Entry <Copy size="1.2em" class="mr-2" /> Clone Entry
</button> </button>
</section> </section>
<section class="space-y-4 pt-4 border-t border-surface-500/20"> <section class="border-surface-500/20 space-y-4 border-t pt-4">
<h4 class="text-xs font-bold uppercase opacity-50"> <h4 class="text-xs font-bold uppercase opacity-50">
Quick Category Quick Category
</h4> </h4>
@@ -211,8 +223,7 @@
tmp_entry_obj.category_code = cat.code; tmp_entry_obj.category_code = cat.code;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}>
>
{cat.name} {cat.name}
</button> </button>
{/each} {/each}
@@ -220,7 +231,7 @@
</section> </section>
</div> </div>
{:else if tab === 'meta'} {:else if tab === 'meta'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70">Category</span> <span class="text-sm font-bold opacity-70">Category</span>
<select <select
@@ -229,8 +240,7 @@
onchange={() => { onchange={() => {
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}>
>
<option value="">None</option> <option value="">None</option>
{#each journal?.cfg_json?.category_li ?? [] as cat (cat.code)} {#each journal?.cfg_json?.category_li ?? [] as cat (cat.code)}
<option value={cat.code}>{cat.name}</option> <option value={cat.code}>{cat.name}</option>
@@ -240,9 +250,8 @@
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Tags (Comma separated)</span >Tags (Comma separated)</span>
> <div class="flex items-center gap-2">
<div class="flex gap-2 items-center">
<Tag size="1.2em" class="opacity-30" /> <Tag size="1.2em" class="opacity-30" />
<input <input
type="text" type="text"
@@ -252,16 +261,14 @@
onchange={() => { onchange={() => {
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }} />
/>
</div> </div>
</label> </label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Archive On</span >Archive On</span>
>
<input <input
type="datetime-local" type="datetime-local"
value={normalize_date(tmp_entry_obj.archive_on)} value={normalize_date(tmp_entry_obj.archive_on)}
@@ -271,13 +278,11 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="input" class="input" />
/>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Sort Priority</span >Sort Priority</span>
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
@@ -287,11 +292,9 @@
(tmp_entry_obj.sort ?? 0) - 1; (tmp_entry_obj.sort ?? 0) - 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Minus size="1em" /></button }}><Minus size="1em" /></button>
> <span class="w-8 text-center font-mono font-bold"
<span class="font-mono font-bold w-8 text-center" >{tmp_entry_obj.sort ?? 0}</span>
>{tmp_entry_obj.sort ?? 0}</span
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
@@ -300,25 +303,22 @@
(tmp_entry_obj.sort ?? 0) + 1; (tmp_entry_obj.sort ?? 0) + 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Plus size="1em" /></button }}><Plus size="1em" /></button>
>
</div> </div>
</label> </label>
</div> </div>
</div> </div>
{:else if tab === 'security'} {:else if tab === 'security'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Fingerprint size="1.2em" class="text-primary-500" /> <Fingerprint size="1.2em" class="text-primary-500" />
Status & Security Status & Security
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.enable} bind:checked={tmp_entry_obj.enable}
@@ -326,18 +326,15 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Enabled</span> <span class="font-bold">Enabled</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Allow access to this entry</span >Allow access to this entry</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.hide} bind:checked={tmp_entry_obj.hide}
@@ -345,18 +342,15 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Hidden</span> <span class="font-bold">Hidden</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Hide from standard lists</span >Hide from standard lists</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.priority} bind:checked={tmp_entry_obj.priority}
@@ -364,24 +358,20 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Priority Entry</span> <span class="font-bold">Priority Entry</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Star or pin to top</span >Star or pin to top</span>
>
</div> </div>
</label> </label>
<div <div
class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold text-sm">Sort Order</span <span class="text-sm font-bold"
> >Sort Order</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Manual list position</span >Manual list position</span>
>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
@@ -392,12 +382,10 @@
(tmp_entry_obj.sort ?? 0) - 1; (tmp_entry_obj.sort ?? 0) - 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Minus size="1em" /></button }}><Minus size="1em" /></button>
>
<span <span
class="font-mono font-bold w-8 text-center text-lg" class="w-8 text-center font-mono text-lg font-bold"
>{tmp_entry_obj.sort ?? 0}</span >{tmp_entry_obj.sort ?? 0}</span>
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
@@ -406,8 +394,7 @@
(tmp_entry_obj.sort ?? 0) + 1; (tmp_entry_obj.sort ?? 0) + 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Plus size="1em" /></button }}><Plus size="1em" /></button>
>
</div> </div>
</div> </div>
</div> </div>
@@ -429,21 +416,18 @@
hide_personal={journal?.cfg_json?.hide_btn_personal} hide_personal={journal?.cfg_json?.hide_btn_personal}
hide_professional={journal?.cfg_json hide_professional={journal?.cfg_json
?.hide_btn_professional} ?.hide_btn_professional}
hide_template={journal?.cfg_json?.hide_btn_template} hide_template={journal?.cfg_json?.hide_btn_template} />
/>
</section> </section>
{#if tmp_entry_obj.private && !tmp_entry_obj.content && tmp_entry_obj.content_encrypted} {#if tmp_entry_obj.private && !tmp_entry_obj.content && tmp_entry_obj.content_encrypted}
<section class="pt-8 border-t border-error-500/20"> <section class="border-error-500/20 border-t pt-8">
<div <div
class="bg-error-500/10 p-4 rounded-lg border border-error-500/30" class="bg-error-500/10 border-error-500/30 rounded-lg border p-4">
>
<h4 <h4
class="text-error-500 font-bold flex items-center gap-2 mb-2" class="text-error-500 mb-2 flex items-center gap-2 font-bold">
>
<RefreshCcw size="1.2em" /> Disaster Recovery <RefreshCcw size="1.2em" /> Disaster Recovery
</h4> </h4>
<p class="text-xs opacity-70 mb-4 italic"> <p class="mb-4 text-xs italic opacity-70">
If the encryption passcode is lost, the data is If the encryption passcode is lost, the data is
unrecoverable. You can force a reset to plain unrecoverable. You can force a reset to plain
text to reuse this entry ID. text to reuse this entry ID.
@@ -454,8 +438,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_force_reset?.(); on_force_reset?.();
}} }}>
>
Force Reset to Plain Text Force Reset to Plain Text
</button> </button>
</div> </div>
@@ -468,8 +451,7 @@
class="btn btn-sm preset-tonal-error w-full" class="btn btn-sm preset-tonal-error w-full"
onclick={() => { onclick={() => {
alert('Delete logic handled in parent component'); alert('Delete logic handled in parent component');
}} }}>
>
<Trash2 size="1.1em" class="mr-2" /> Delete Entry <Trash2 size="1.1em" class="mr-2" /> Delete Entry
</button> </button>
</section> </section>
@@ -480,8 +462,7 @@
readonly={true} readonly={true}
content={JSON.stringify(tmp_entry_obj, null, 2)} content={JSON.stringify(tmp_entry_obj, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -489,9 +470,8 @@
{#snippet footer()} {#snippet footer()}
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold min-w-[120px]" class="btn preset-filled-primary min-w-[120px] font-bold"
onclick={() => (show = false)} onclick={() => (show = false)}>
>
<Check size="1.2em" class="mr-2" /> <Check size="1.2em" class="mr-2" />
Done Done
</button> </button>

View File

@@ -1,107 +1,113 @@
<script lang="ts"> <script lang="ts">
/** /**
* @file ae_comp__modal_journal_export.svelte * @file ae_comp__modal_journal_export.svelte
* @description Modal component for bulk exporting journal entries. * @description Modal component for bulk exporting journal entries.
* Allows exporting entries using various templates (Markdown, HTML, JSON). * Allows exporting entries using various templates (Markdown, HTML, JSON).
* @author One Sky IT * @author One Sky IT
*/ */
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Code, Copy, Download, FileJson, FileType, Settings2 } from '@lucide/svelte'; import {
import { ae_util } from '$lib/ae_utils/ae_utils'; Code,
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; Copy,
import { Download,
EXPORT_TEMPLATES, FileJson,
type ExportTemplate FileType,
} from '$lib/ae_journals/ae_journals_export_templates'; Settings2
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
import {
EXPORT_TEMPLATES,
type ExportTemplate
} from '$lib/ae_journals/ae_journals_export_templates';
interface Props { interface Props {
open: boolean; open: boolean;
entries: ae_JournalEntry[]; entries: ae_JournalEntry[];
journal?: ae_Journal; journal?: ae_Journal;
on_close: () => void; on_close: () => void;
} }
let { let {
open = $bindable(false), open = $bindable(false),
entries = [], entries = [],
journal, journal,
on_close on_close
}: Props = $props(); }: Props = $props();
// State // State
let selected_template_id: string = $state('standard_markdown'); let selected_template_id: string = $state('standard_markdown');
let export_preview: string = $state(''); let export_preview: string = $state('');
let export_count: number = $derived(entries.length); let export_count: number = $derived(entries.length);
const templates = Object.values(EXPORT_TEMPLATES); const templates = Object.values(EXPORT_TEMPLATES);
const selected_template = $derived( const selected_template = $derived(
EXPORT_TEMPLATES[ EXPORT_TEMPLATES[selected_template_id as keyof typeof EXPORT_TEMPLATES] ||
selected_template_id as keyof typeof EXPORT_TEMPLATES EXPORT_TEMPLATES.standard_markdown
] || EXPORT_TEMPLATES.standard_markdown );
);
// Auto-select template based on journal type when modal opens // Auto-select template based on journal type when modal opens
$effect(() => { $effect(() => {
if (open && journal?.type_code) { if (open && journal?.type_code) {
if ( if (
journal.type_code === 'personal_log' && journal.type_code === 'personal_log' &&
EXPORT_TEMPLATES.personal_log EXPORT_TEMPLATES.personal_log
) { ) {
selected_template_id = 'personal_log'; selected_template_id = 'personal_log';
} else if ( } else if (
journal.type_code === 'amazon_vine' && journal.type_code === 'amazon_vine' &&
EXPORT_TEMPLATES.amazon_vine EXPORT_TEMPLATES.amazon_vine
) { ) {
selected_template_id = 'amazon_vine'; selected_template_id = 'amazon_vine';
}
} }
}
});
// Re-generate preview when entries or template changes
$effect(() => {
if (open && entries.length > 0) {
generate_preview();
}
});
function generate_preview() {
if (!selected_template) return;
export_preview = selected_template.formatter(entries);
}
/**
* Downloads the generated content as a file.
*/
function handle_download() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `journal_export_${timestamp}.${selected_template.extension}`;
const blob = new Blob([export_preview], {
type: 'text/plain;charset=utf-8'
}); });
// Re-generate preview when entries or template changes const url = URL.createObjectURL(blob);
$effect(() => { const link = document.createElement('a');
if (open && entries.length > 0) { link.href = url;
generate_preview(); link.download = filename;
} document.body.appendChild(link);
}); link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function generate_preview() { /**
if (!selected_template) return; * Copies the generated content to the clipboard.
export_preview = selected_template.formatter(entries); */
} async function handle_copy() {
try {
/** await navigator.clipboard.writeText(export_preview);
* Downloads the generated content as a file. alert('Content copied to clipboard!');
*/ } catch (err) {
function handle_download() { console.error('Failed to copy:', err);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); alert('Failed to copy to clipboard.');
const filename = `journal_export_${timestamp}.${selected_template.extension}`;
const blob = new Blob([export_preview], {
type: 'text/plain;charset=utf-8'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Copies the generated content to the clipboard.
*/
async function handle_copy() {
try {
await navigator.clipboard.writeText(export_preview);
alert('Content copied to clipboard!');
} catch (err) {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard.');
}
} }
}
</script> </script>
<Modal <Modal
@@ -109,12 +115,10 @@
bind:open bind:open
autoclose={false} autoclose={false}
size="xl" size="xl"
class="w-full" class="w-full">
>
<div class="space-y-4"> <div class="space-y-4">
<div <div
class="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 rounded-lg" class="flex items-center justify-between rounded-lg bg-blue-50 p-4 text-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
>
<div> <div>
<strong>Ready to Export:</strong> <strong>Ready to Export:</strong>
{export_count} entries. {export_count} entries.
@@ -122,8 +126,7 @@
{#if journal} {#if journal}
<div class="text-xs opacity-70"> <div class="text-xs opacity-70">
Journal Type: <span class="font-mono" Journal Type: <span class="font-mono"
>{journal.type_code || 'standard'}</span >{journal.type_code || 'standard'}</span>
>
</div> </div>
{/if} {/if}
</div> </div>
@@ -134,19 +137,18 @@
<Settings2 size="1em" /> <Settings2 size="1em" />
<span class="label-text font-bold">Select Export Template</span> <span class="label-text font-bold">Select Export Template</span>
</label> </label>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2"> <div class="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-5">
{#each templates as template (template.id)} {#each templates as template (template.id)}
<button <button
type="button" type="button"
class="btn preset-outlined-surface flex flex-col gap-1 p-2 h-24 items-center justify-center border-2 text-center {selected_template_id === class="btn preset-outlined-surface flex h-24 flex-col items-center justify-center gap-1 border-2 p-2 text-center {selected_template_id ===
template.id template.id
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'opacity-60'}" : 'opacity-60'}"
onclick={() => { onclick={() => {
selected_template_id = template.id; selected_template_id = template.id;
generate_preview(); generate_preview();
}} }}>
>
{#if template.extension === 'json'} {#if template.extension === 'json'}
<FileJson size="1.5em" /> <FileJson size="1.5em" />
{:else if template.extension === 'html'} {:else if template.extension === 'html'}
@@ -154,12 +156,10 @@
{:else} {:else}
<FileType size="1.5em" /> <FileType size="1.5em" />
{/if} {/if}
<span class="text-xs font-bold leading-tight" <span class="text-xs leading-tight font-bold"
>{template.name}</span >{template.name}</span>
>
<span class="text-[10px] opacity-70" <span class="text-[10px] opacity-70"
>.{template.extension}</span >.{template.extension}</span>
>
</button> </button>
{/each} {/each}
</div> </div>
@@ -167,43 +167,38 @@
<!-- Preview Area --> <!-- Preview Area -->
<div class="form-control"> <div class="form-control">
<div class="label flex justify-between items-center"> <div class="label flex items-center justify-between">
<span class="label-text">Preview</span> <span class="label-text">Preview</span>
<span class="text-[10px] opacity-50 uppercase tracking-widest" <span class="text-[10px] tracking-widest uppercase opacity-50"
>{selected_template.name}</span >{selected_template.name}</span>
>
</div> </div>
<textarea <textarea
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900" class="textarea h-64 bg-gray-50 font-mono text-xs dark:bg-gray-900"
readonly readonly
value={export_preview.substring(0, 5000) + value={export_preview.substring(0, 5000) +
(export_preview.length > 5000 (export_preview.length > 5000
? '\n... (truncated for preview)' ? '\n... (truncated for preview)'
: '')} : '')}></textarea>
></textarea>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="modal-action flex justify-between items-center"> <div class="modal-action flex items-center justify-between">
<button <button
type="button" type="button"
class="btn preset-tonal-secondary" class="btn preset-tonal-secondary"
onclick={on_close}>Close</button onclick={on_close}>Close</button>
>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
type="button" type="button"
class="btn preset-tonal-primary" class="btn preset-tonal-primary"
onclick={handle_copy} onclick={handle_copy}>
>
<Copy class="mr-2" size="1.2em" /> Copy to Clipboard <Copy class="mr-2" size="1.2em" /> Copy to Clipboard
</button> </button>
<button <button
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
onclick={handle_download} onclick={handle_download}>
>
<Download class="mr-2" size="1.2em" /> Download File <Download class="mr-2" size="1.2em" /> Download File
</button> </button>
</div> </div>

View File

@@ -1,142 +1,145 @@
<script lang="ts"> <script lang="ts">
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Check, CircleAlert, FileText, RefreshCw, Upload, X } from '@lucide/svelte'; import {
import { Check,
PARSERS, CircleAlert,
type AeJournalEntryInput FileText,
} from '$lib/ae_journals/ae_journals_parsers'; RefreshCw,
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; Upload,
import { ae_api } from '$lib/stores/ae_stores'; X
import { journals_slct } from '$lib/ae_journals/ae_journals_stores'; } from '@lucide/svelte';
import {
PARSERS,
type AeJournalEntryInput
} from '$lib/ae_journals/ae_journals_parsers';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores';
import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
interface Props { interface Props {
open: boolean; open: boolean;
on_close: () => void; on_close: () => void;
on_import_complete: () => void; on_import_complete: () => void;
}
let { open = $bindable(false), on_close, on_import_complete }: Props = $props();
let files: FileList | null = $state(null);
let selected_parser: keyof typeof PARSERS = $state('standard');
let parsed_entries: AeJournalEntryInput[] = $state([]);
let is_parsing = $state(false);
let is_importing = $state(false);
let import_log: string[] = $state([]);
let is_dragging = $state(false);
// Watch for file selection or parser change to trigger parsing
$effect(() => {
if (files && files.length > 0) {
parse_files();
} }
});
let { function handle_drag_enter(e: DragEvent) {
open = $bindable(false), e.preventDefault();
on_close, e.stopPropagation();
on_import_complete is_dragging = true;
}: Props = $props(); }
let files: FileList | null = $state(null); function handle_drag_leave(e: DragEvent) {
let selected_parser: keyof typeof PARSERS = $state('standard'); e.preventDefault();
let parsed_entries: AeJournalEntryInput[] = $state([]); e.stopPropagation();
let is_parsing = $state(false); is_dragging = false;
let is_importing = $state(false); }
let import_log: string[] = $state([]);
let is_dragging = $state(false);
// Watch for file selection or parser change to trigger parsing function handle_drag_over(e: DragEvent) {
$effect(() => { e.preventDefault();
if (files && files.length > 0) { e.stopPropagation();
parse_files(); is_dragging = true;
} }
});
function handle_drag_enter(e: DragEvent) { function handle_drop(e: DragEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
is_dragging = true; is_dragging = false;
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
files = e.dataTransfer.files;
} }
}
function handle_drag_leave(e: DragEvent) { async function parse_files() {
e.preventDefault(); if (!files) return;
e.stopPropagation(); is_parsing = true;
is_dragging = false; parsed_entries = [];
}
function handle_drag_over(e: DragEvent) { const parser = PARSERS[selected_parser];
e.preventDefault();
e.stopPropagation();
is_dragging = true;
}
function handle_drop(e: DragEvent) { for (let i = 0; i < files.length; i++) {
e.preventDefault(); const file = files[i];
e.stopPropagation(); try {
is_dragging = false; const text = await file.text();
const entries = await parser(file, text);
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { parsed_entries = [...parsed_entries, ...entries];
files = e.dataTransfer.files; } catch (err) {
console.error(`Error parsing ${file.name}:`, err);
} }
} }
is_parsing = false;
}
async function parse_files() { async function handle_import() {
if (!files) return; if (parsed_entries.length === 0) return;
is_parsing = true; is_importing = true;
parsed_entries = []; import_log = [];
const parser = PARSERS[selected_parser]; const journal_id = $journals_slct.journal_id;
if (!journal_id) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const text = await file.text();
const entries = await parser(file, text);
parsed_entries = [...parsed_entries, ...entries];
} catch (err) {
console.error(`Error parsing ${file.name}:`, err);
}
}
is_parsing = false;
}
async function handle_import() {
if (parsed_entries.length === 0) return;
is_importing = true;
import_log = [];
const journal_id = $journals_slct.journal_id;
if (!journal_id) {
alert(
'No target journal selected. Please select a journal in the background first.'
);
is_importing = false;
return;
}
let success_count = 0;
for (const entry of parsed_entries) {
try {
// Construct payload
const data_kv = {
name: entry.name,
content: entry.content,
tags: entry.tags.join(', '),
type_code: entry.type_code || 'note',
created_on: entry.created_on,
updated_on: entry.updated_on
};
const res = await journals_func.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id: journal_id,
data_kv: data_kv,
log_lvl: 0
});
if (res) {
import_log.push(`✅ Imported: ${entry.name}`);
success_count++;
} else {
import_log.push(`❌ Failed: ${entry.name}`);
}
} catch (err) {
import_log.push(`❌ Error: ${entry.name} - ${err}`);
}
}
is_importing = false;
alert( alert(
`Import complete! ${success_count}/${parsed_entries.length} imported.` 'No target journal selected. Please select a journal in the background first.'
); );
on_import_complete(); is_importing = false;
open = false; return;
} }
let success_count = 0;
for (const entry of parsed_entries) {
try {
// Construct payload
const data_kv = {
name: entry.name,
content: entry.content,
tags: entry.tags.join(', '),
type_code: entry.type_code || 'note',
created_on: entry.created_on,
updated_on: entry.updated_on
};
const res = await journals_func.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id: journal_id,
data_kv: data_kv,
log_lvl: 0
});
if (res) {
import_log.push(`✅ Imported: ${entry.name}`);
success_count++;
} else {
import_log.push(`❌ Failed: ${entry.name}`);
}
} catch (err) {
import_log.push(`❌ Error: ${entry.name} - ${err}`);
}
}
is_importing = false;
alert(
`Import complete! ${success_count}/${parsed_entries.length} imported.`
);
on_import_complete();
open = false;
}
</script> </script>
<Modal <Modal
@@ -144,21 +147,18 @@
bind:open bind:open
autoclose={false} autoclose={false}
size="xl" size="xl"
class="w-full" class="w-full">
>
<div class="space-y-4"> <div class="space-y-4">
<!-- Configuration --> <!-- Configuration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div> <div>
<label class="label"> <label class="label">
<span>Parser Strategy</span> <span>Parser Strategy</span>
<select class="select" bind:value={selected_parser}> <select class="select" bind:value={selected_parser}>
<option value="standard" <option value="standard"
>Standard (1 File = 1 Entry)</option >Standard (1 File = 1 Entry)</option>
>
<option value="personal_log" <option value="personal_log"
>Personal Log (Split by Date)</option >Personal Log (Split by Date)</option>
>
<option value="amazon_vine">Amazon Vine Reviews</option> <option value="amazon_vine">Amazon Vine Reviews</option>
</select> </select>
</label> </label>
@@ -173,25 +173,23 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class=" class="
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all duration-200 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2
flex flex-col items-center justify-center gap-2 border-dashed p-8 text-center transition-all duration-200
{is_dragging {is_dragging
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'} : 'border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500'}
" "
ondragenter={handle_drag_enter} ondragenter={handle_drag_enter}
ondragleave={handle_drag_leave} ondragleave={handle_drag_leave}
ondragover={handle_drag_over} ondragover={handle_drag_over}
ondrop={handle_drop} ondrop={handle_drop}
onclick={() => onclick={() =>
document.getElementById('file_import_input')?.click()} document.getElementById('file_import_input')?.click()}>
>
<Upload class="h-10 w-10 text-gray-400" /> <Upload class="h-10 w-10 text-gray-400" />
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<span <span
class="font-semibold text-primary-600 hover:text-primary-500" class="text-primary-600 hover:text-primary-500 font-semibold"
>Click to upload</span >Click to upload</span>
>
or drag and drop or drag and drop
</p> </p>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
@@ -204,17 +202,15 @@
class="hidden" class="hidden"
multiple multiple
accept=".md,.txt" accept=".md,.txt"
onchange={(e) => (files = e.currentTarget.files)} onchange={(e) => (files = e.currentTarget.files)} />
/>
</div> </div>
</div> </div>
</div> </div>
<!-- Preview --> <!-- Preview -->
<div <div
class="border rounded-lg p-2 bg-gray-50 dark:bg-gray-900 max-h-64 overflow-y-auto" class="max-h-64 overflow-y-auto rounded-lg border bg-gray-50 p-2 dark:bg-gray-900">
> <h4 class="mb-2 flex justify-between font-bold">
<h4 class="font-bold mb-2 flex justify-between">
<span>Preview ({parsed_entries.length} entries)</span> <span>Preview ({parsed_entries.length} entries)</span>
{#if is_parsing} {#if is_parsing}
<RefreshCw class="animate-spin" /> <RefreshCw class="animate-spin" />
@@ -222,7 +218,7 @@
</h4> </h4>
{#if parsed_entries.length > 0} {#if parsed_entries.length > 0}
<table class="table table-compact w-full text-xs"> <table class="table-compact table w-full text-xs">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -234,9 +230,8 @@
{#each parsed_entries as entry, i (i)} {#each parsed_entries as entry, i (i)}
<tr> <tr>
<td <td
class="truncate max-w-[200px]" class="max-w-[200px] truncate"
title={entry.name}>{entry.name}</td title={entry.name}>{entry.name}</td>
>
<td>{entry.created_on?.substring(0, 10)}</td> <td>{entry.created_on?.substring(0, 10)}</td>
<td>{entry.tags.join(', ')}</td> <td>{entry.tags.join(', ')}</td>
</tr> </tr>
@@ -244,11 +239,11 @@
</tbody> </tbody>
</table> </table>
{:else if files && files.length > 0 && !is_parsing} {:else if files && files.length > 0 && !is_parsing}
<div class="text-center text-gray-500 py-4"> <div class="py-4 text-center text-gray-500">
No entries found in selected files. No entries found in selected files.
</div> </div>
{:else if !files} {:else if !files}
<div class="text-center text-gray-500 py-4"> <div class="py-4 text-center text-gray-500">
Select files to preview import. Select files to preview import.
</div> </div>
{/if} {/if}
@@ -257,8 +252,7 @@
<!-- Import Log --> <!-- Import Log -->
{#if import_log.length > 0} {#if import_log.length > 0}
<div <div
class="bg-black text-green-400 p-2 rounded text-xs font-mono max-h-32 overflow-y-auto" class="max-h-32 overflow-y-auto rounded bg-black p-2 font-mono text-xs text-green-400">
>
{#each import_log as log, i (i)} {#each import_log as log, i (i)}
<div>{log}</div> <div>{log}</div>
{/each} {/each}
@@ -269,14 +263,12 @@
<button <button
type="button" type="button"
class="btn preset-tonal-secondary" class="btn preset-tonal-secondary"
onclick={on_close}>Cancel</button onclick={on_close}>Cancel</button>
>
<button <button
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
disabled={parsed_entries.length === 0 || is_importing} disabled={parsed_entries.length === 0 || is_importing}
onclick={handle_import} onclick={handle_import}>
>
{#if is_importing} {#if is_importing}
Importing... Importing...
{:else} {:else}

View File

@@ -1,105 +1,104 @@
<script lang="ts"> <script lang="ts">
// *** Import Svelte specific // *** Import Svelte specific
import { import {
ArrowDown01, ArrowDown01,
ArrowDown10, ArrowDown10,
ArrowDownUp, ArrowDownUp,
BookHeart, BookHeart,
BriefcaseBusiness, BriefcaseBusiness,
CalendarClock, CalendarClock,
CalendarOff, CalendarOff,
Clock, Clock,
CodeXml, CodeXml,
Copy, Copy,
Eye, Eye,
EyeOff, EyeOff,
Flag, Flag,
FlagOff, FlagOff,
FileX, FileX,
Fingerprint, Fingerprint,
Globe, Globe,
Group, Group,
History, History,
LockKeyhole, LockKeyhole,
LockKeyholeOpen, LockKeyholeOpen,
MessageSquareWarning, MessageSquareWarning,
Minus, Minus,
NotebookPen, NotebookPen,
NotebookText, NotebookText,
NotepadTextDashed, NotepadTextDashed,
Pencil, Pencil,
PenLine, PenLine,
Plus, Plus,
RemoveFormatting, RemoveFormatting,
Search, Search,
Shapes, Shapes,
Share2, Share2,
ShieldCheck, ShieldCheck,
ShieldMinus, ShieldMinus,
Siren, Siren,
Skull, Skull,
SquareLibrary, SquareLibrary,
Tags, Tags,
Trash2, Trash2,
TypeOutline, TypeOutline,
X X
} from '@lucide/svelte'; } from '@lucide/svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
ae_api, ae_api,
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { journals_slct } from '$lib/ae_journals/ae_journals_stores'; import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
obj_priority: any; obj_priority: any;
obj_sort: any; obj_sort: any;
obj_group: any; obj_group: any;
obj_archive_on: any; obj_archive_on: any;
obj_hide: any; obj_hide: any;
obj_enable: any; obj_enable: any;
obj_delete: any; obj_delete: any;
tmp_entry_obj: any; tmp_entry_obj: any;
update_journal_entry: any; update_journal_entry: any;
lq__journal_entry_obj: any; lq__journal_entry_obj: any;
change_journal_id: any; change_journal_id: any;
lq__journal_obj_li: any; lq__journal_obj_li: any;
} }
let { let {
log_lvl = 0, log_lvl = 0,
obj_priority, obj_priority,
obj_sort, obj_sort,
obj_group, obj_group,
obj_archive_on, obj_archive_on,
obj_hide, obj_hide,
obj_enable, obj_enable,
obj_delete, obj_delete,
tmp_entry_obj, tmp_entry_obj,
update_journal_entry, update_journal_entry,
lq__journal_entry_obj, lq__journal_entry_obj,
change_journal_id, change_journal_id,
lq__journal_obj_li lq__journal_obj_li
}: Props = $props(); }: Props = $props();
let ae_promises: key_val = $state({}); let ae_promises: key_val = $state({});
</script> </script>
<section <section
class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-between w-full" class="ae_meta flex w-full flex-row flex-wrap items-center justify-between gap-1">
>
<!-- {lq__journal_entry_obj?.priority} <!-- {lq__journal_entry_obj?.priority}
{lq__journal_entry_obj?.sort} {lq__journal_entry_obj?.sort}
{lq__journal_entry_obj?.group} {lq__journal_entry_obj?.group}
@@ -113,9 +112,8 @@
obj_priority = !obj_priority; obj_priority = !obj_priority;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm md:btn-icon-base preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm md:btn-icon-base preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Toggle priority of this journal entry" title="Toggle priority of this journal entry">
>
{#if obj_priority} {#if obj_priority}
<Flag strokeWidth="2.5" color="green" class="inline-block" /> <Flag strokeWidth="2.5" color="green" class="inline-block" />
{:else} {:else}
@@ -127,17 +125,15 @@
<!-- Set sort order (number) --> <!-- Set sort order (number) -->
<span <span
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="flex flex-row flex-wrap items-center justify-center border border-gray-300 rounded-lg" class="flex flex-row flex-wrap items-center justify-center rounded-lg border border-gray-300">
>
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
obj_sort = obj_sort ? obj_sort + 1 : 1; obj_sort = obj_sort ? obj_sort + 1 : 1;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Increment sort order of this journal entry" title="Increment sort order of this journal entry">
>
<Plus strokeWidth="2.5" color="blue" /> <Plus strokeWidth="2.5" color="blue" />
</button> </button>
<span class="mx-1"> <span class="mx-1">
@@ -154,9 +150,8 @@
obj_sort = obj_sort ? obj_sort - 1 : 0; obj_sort = obj_sort ? obj_sort - 1 : 0;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Decrement sort order of this journal entry" title="Decrement sort order of this journal entry">
>
<Minus strokeWidth="2.5" color="blue" /> <Minus strokeWidth="2.5" color="blue" />
</button> </button>
</span> </span>
@@ -171,13 +166,11 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="input input-sm input-bordered w-24" class="input input-sm input-bordered w-24"
title="Set group (for sorting) of this journal entry" title="Set group (for sorting) of this journal entry" />
/>
<!-- Set archive datetime (string) --> <!-- Set archive datetime (string) -->
<span <span
class="flex flex-row flex-wrap items-center justify-center border border-gray-200 rounded-lg" class="flex flex-row flex-wrap items-center justify-center rounded-lg border border-gray-200">
>
<input <input
type="datetime-local" type="datetime-local"
bind:value={obj_archive_on} bind:value={obj_archive_on}
@@ -187,8 +180,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="input input-sm input-bordered w-auto border-none" class="input input-sm input-bordered w-auto border-none"
title="Set archive on datetime for archiving this journal entry" title="Set archive on datetime for archiving this journal entry" />
/>
{#if obj_archive_on} {#if obj_archive_on}
<!-- Button to clear the datetime --> <!-- Button to clear the datetime -->
@@ -200,8 +192,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Clear the archive on datetime for this journal entry" title="Clear the archive on datetime for this journal entry">
>
<X strokeWidth="2.5" color="red" /> <X strokeWidth="2.5" color="red" />
<!-- <span class="hidden">Clear Archive</span> --> <!-- <span class="hidden">Clear Archive</span> -->
</button> </button>
@@ -221,8 +212,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Set the archive on datetime for this journal entry" title="Set the archive on datetime for this journal entry">
>
<Clock strokeWidth="2.5" color="blue" /> <Clock strokeWidth="2.5" color="blue" />
<!-- <span class="hidden">Set Archive</span> --> <!-- <span class="hidden">Set Archive</span> -->
</button> </button>
@@ -237,8 +227,7 @@
update_journal_entry(); update_journal_entry();
}} }}
class="btn btn-sm md:btn-md preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn btn-sm md:btn-md preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Toggle visibility of this journal entry" title="Toggle visibility of this journal entry">
>
{#if obj_hide} {#if obj_hide}
<EyeOff strokeWidth="1" color="red" class="inline-block" /> <EyeOff strokeWidth="1" color="red" class="inline-block" />
<span class="hidden md:inline">Hidden</span> <span class="hidden md:inline">Hidden</span>
@@ -257,8 +246,7 @@
}} }}
class:hidden={!$ae_loc.administrator_access || !$ae_loc.edit_mode} class:hidden={!$ae_loc.administrator_access || !$ae_loc.edit_mode}
class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition" class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition"
title="Toggle enable status of this journal entry" title="Toggle enable status of this journal entry">
>
{#if obj_enable} {#if obj_enable}
<ShieldCheck strokeWidth="2.5" color="green" class="inline-block" /> <ShieldCheck strokeWidth="2.5" color="green" class="inline-block" />
<span class="hidden md:inline">Enabled</span> <span class="hidden md:inline">Enabled</span>
@@ -304,8 +292,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition" class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition"
title="Delete this journal entry" title="Delete this journal entry">
>
{#if $ae_loc.administrator_access && $ae_loc.edit_mode} {#if $ae_loc.administrator_access && $ae_loc.edit_mode}
<FileX strokeWidth="2.5" color="red" class="inline-block" /> <FileX strokeWidth="2.5" color="red" class="inline-block" />
<span class="hidden md:inline">Delete</span> <span class="hidden md:inline">Delete</span>
@@ -316,8 +303,7 @@
</button> </button>
<span <span
class="flex flex-row items-center justify-center text-sm text-gray-500" class="flex flex-row items-center justify-center text-sm text-gray-500">
>
{#if !$ae_loc.edit_mode} {#if !$ae_loc.edit_mode}
<span class=""> <span class="">
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
@@ -344,19 +330,18 @@
<!-- Select option list of Journals to choose from. This is used to assign the Journal Entry to a different Journal ID. --> <!-- Select option list of Journals to choose from. This is used to assign the Journal Entry to a different Journal ID. -->
{#if $ae_loc.edit_mode && lq__journal_obj_li?.length} {#if $ae_loc.edit_mode && lq__journal_obj_li?.length}
<div <div
class="flex flex-row flex-wrap gap-2 items-center justify-start border border-gray-200 rounded-lg" class="flex flex-row flex-wrap items-center justify-start gap-2 rounded-lg border border-gray-200">
>
<SquareLibrary size="1em" class="mx-1" /> <SquareLibrary size="1em" class="mx-1" />
<span class="text-sm text-gray-500 hidden sm:inline"> <span class="hidden text-sm text-gray-500 sm:inline">
Journal: Journal:
</span> </span>
<select <select
class="novi_btn btn btn-secondary btn-sm class="novi_btn btn btn-secondary btn-sm
preset-tonal-primary preset-tonal-primary
hover:preset-filled-primary-500 hover:preset-filled-primary-500
transition
text-xs
border-none border-none
text-xs
transition
" "
bind:value={tmp_entry_obj.journal_id} bind:value={tmp_entry_obj.journal_id}
onchange={(event) => { onchange={(event) => {
@@ -372,14 +357,12 @@
change_journal_id(); change_journal_id();
} }
}} }}
title="Select a different journal for this entry" title="Select a different journal for this entry">
>
<option value="">Select Journal</option> <option value="">Select Journal</option>
{#each lq__journal_obj_li as journal (journal.journal_id)} {#each lq__journal_obj_li as journal (journal.journal_id)}
<option <option
value={journal.journal_id} value={journal.journal_id}
title={`Journal: ${journal.name}`} title={`Journal: ${journal.name}`}>
>
{journal.name} {journal.name}
</option> </option>
{/each} {/each}

View File

@@ -30,33 +30,49 @@ describe('Journal Entry Visibility Filtering', () => {
{ id: '1', name: 'Normal Entry', hide: false, enable: true }, { id: '1', name: 'Normal Entry', hide: false, enable: true },
{ id: '2', name: 'Hidden Entry', hide: true, enable: true }, { id: '2', name: 'Hidden Entry', hide: true, enable: true },
{ id: '3', name: 'Disabled Entry', hide: false, enable: false }, { id: '3', name: 'Disabled Entry', hide: false, enable: false },
{ id: '4', name: 'Hidden & Disabled', hide: true, enable: false }, { id: '4', name: 'Hidden & Disabled', hide: true, enable: false }
]; ];
it('should show only normal entries when Edit Mode is OFF (Manager)', () => { it('should show only normal entries when Edit Mode is OFF (Manager)', () => {
const ae_loc = { edit_mode: false, trusted_access: true, administrator_access: true }; const ae_loc = {
edit_mode: false,
trusted_access: true,
administrator_access: true
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(1); expect(result?.length).toBe(1);
expect(result?.[0].id).toBe('1'); expect(result?.[0].id).toBe('1');
}); });
it('should show hidden entries to Trusted users when Edit Mode is ON', () => { it('should show hidden entries to Trusted users when Edit Mode is ON', () => {
const ae_loc = { edit_mode: true, trusted_access: true, administrator_access: false }; const ae_loc = {
edit_mode: true,
trusted_access: true,
administrator_access: false
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
// Should see Normal (1) and Hidden (2). Should NOT see Disabled (3, 4) // Should see Normal (1) and Hidden (2). Should NOT see Disabled (3, 4)
expect(result?.length).toBe(2); expect(result?.length).toBe(2);
expect(result?.map(r => r.id)).toContain('1'); expect(result?.map((r) => r.id)).toContain('1');
expect(result?.map(r => r.id)).toContain('2'); expect(result?.map((r) => r.id)).toContain('2');
}); });
it('should show everything to Administrators when Edit Mode is ON', () => { it('should show everything to Administrators when Edit Mode is ON', () => {
const ae_loc = { edit_mode: true, trusted_access: true, administrator_access: true }; const ae_loc = {
edit_mode: true,
trusted_access: true,
administrator_access: true
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(4); expect(result?.length).toBe(4);
}); });
it('should hide everything sensitive to Public users even if Edit Mode is ON (Safety Check)', () => { it('should hide everything sensitive to Public users even if Edit Mode is ON (Safety Check)', () => {
const ae_loc = { edit_mode: true, trusted_access: false, administrator_access: false }; const ae_loc = {
edit_mode: true,
trusted_access: false,
administrator_access: false
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(1); expect(result?.length).toBe(1);
expect(result?.[0].id).toBe('1'); expect(result?.[0].id).toBe('1');