style(journals): apply expanded 80-width formatting and snake_case
- Batch formatted all Journals module files using Prettier with printWidth: 80. - Refactored preventDefault to prevent_default across all Svelte components. - Standardized line breaks for imports and long attribute lists for better readability. - Ensured consistent snake_case naming for internal identifiers.
This commit is contained in:
@@ -46,7 +46,9 @@ export async function load_ae_obj_id__journal({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_Journal | null> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** load_ae_obj_id__journal() *** journal_id=${journal_id} (SWR)`);
|
||||
console.log(
|
||||
`*** load_ae_obj_id__journal() *** journal_id=${journal_id} (SWR)`
|
||||
);
|
||||
}
|
||||
|
||||
// 1. FAST PATH: Return cached data immediately
|
||||
@@ -54,16 +56,39 @@ export async function load_ae_obj_id__journal({
|
||||
try {
|
||||
const cached = await db_journals.journal.get(journal_id);
|
||||
if (cached) {
|
||||
if (log_lvl) console.log('JOURNAL LOAD: Cache hit. Returning stale data.');
|
||||
_refresh_journal_id_background({
|
||||
api_cfg, journal_id, view, params, try_cache,
|
||||
inc_entry_li, enabled, hidden, limit, offset, order_by_li, log_lvl: 0
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
'JOURNAL LOAD: Cache hit. Returning stale data.'
|
||||
);
|
||||
_refresh_journal_id_background({
|
||||
api_cfg,
|
||||
journal_id,
|
||||
view,
|
||||
params,
|
||||
try_cache,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
log_lvl: 0
|
||||
});
|
||||
if (inc_entry_li && !cached.journal_entry_li) {
|
||||
cached.journal_entry_li = await load_ae_obj_li__journal_entry({
|
||||
api_cfg, for_obj_type: 'journal', for_obj_id: journal_id,
|
||||
enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl
|
||||
});
|
||||
cached.journal_entry_li =
|
||||
await load_ae_obj_li__journal_entry({
|
||||
api_cfg,
|
||||
for_obj_type: 'journal',
|
||||
for_obj_id: journal_id,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
@@ -71,28 +96,76 @@ export async function load_ae_obj_id__journal({
|
||||
}
|
||||
|
||||
// 2. SLOW PATH: Wait for API
|
||||
return await _refresh_journal_id_background({
|
||||
api_cfg, journal_id, view, params, try_cache,
|
||||
inc_entry_li, enabled, hidden, limit, offset, order_by_li, log_lvl
|
||||
return await _refresh_journal_id_background({
|
||||
api_cfg,
|
||||
journal_id,
|
||||
view,
|
||||
params,
|
||||
try_cache,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal background refresh for a single journal
|
||||
*/
|
||||
async function _refresh_journal_id_background({ api_cfg, journal_id, view, params, try_cache, inc_entry_li, enabled, hidden, limit, offset, order_by_li, log_lvl }: any) {
|
||||
async function _refresh_journal_id_background({
|
||||
api_cfg,
|
||||
journal_id,
|
||||
view,
|
||||
params,
|
||||
try_cache,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
log_lvl
|
||||
}: any) {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return null;
|
||||
try {
|
||||
const result = await api.get_ae_obj_v3({ api_cfg, obj_type: 'journal', obj_id: journal_id, view, params, log_lvl });
|
||||
const result = await api.get_ae_obj_v3({
|
||||
api_cfg,
|
||||
obj_type: 'journal',
|
||||
obj_id: journal_id,
|
||||
view,
|
||||
params,
|
||||
log_lvl
|
||||
});
|
||||
if (result) {
|
||||
if (try_cache) {
|
||||
const processed = await process_ae_obj__journal_props({ obj_li: [result], log_lvl });
|
||||
await db_save_ae_obj_li__ae_obj({ db_instance: db_journals, table_name: 'journal', obj_li: processed, properties_to_save, log_lvl });
|
||||
const processed = await process_ae_obj__journal_props({
|
||||
obj_li: [result],
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
if (inc_entry_li) {
|
||||
result.journal_entry_li = await load_ae_obj_li__journal_entry({
|
||||
api_cfg, for_obj_type: 'journal', for_obj_id: journal_id,
|
||||
enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl
|
||||
api_cfg,
|
||||
for_obj_type: 'journal',
|
||||
for_obj_id: journal_id,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@@ -138,18 +211,37 @@ export async function load_ae_obj_li__journal({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_Journal[]> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** load_ae_obj_li__journal() *** for=${for_obj_type}:${for_obj_id} (SWR)`);
|
||||
console.log(
|
||||
`*** load_ae_obj_li__journal() *** for=${for_obj_type}:${for_obj_id} (SWR)`
|
||||
);
|
||||
}
|
||||
|
||||
// 1. FAST PATH: Check cache
|
||||
if (try_cache) {
|
||||
try {
|
||||
const cached_li = await db_journals.journal.where('for_id').equals(for_obj_id).toArray();
|
||||
const cached_li = await db_journals.journal
|
||||
.where('for_id')
|
||||
.equals(for_obj_id)
|
||||
.toArray();
|
||||
if (cached_li && cached_li.length > 0) {
|
||||
if (log_lvl) console.log(`JOURNAL LIST: Cache hit (${cached_li.length}).`);
|
||||
_refresh_journal_li_background({
|
||||
api_cfg, for_obj_type, for_obj_id, qry_person_id, inc_entry_li,
|
||||
enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl: 0
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`JOURNAL LIST: Cache hit (${cached_li.length}).`
|
||||
);
|
||||
_refresh_journal_li_background({
|
||||
api_cfg,
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
qry_person_id,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl: 0
|
||||
});
|
||||
return cached_li;
|
||||
}
|
||||
@@ -157,39 +249,110 @@ export async function load_ae_obj_li__journal({
|
||||
}
|
||||
|
||||
// 2. SLOW PATH: API
|
||||
return await _refresh_journal_li_background({
|
||||
api_cfg, for_obj_type, for_obj_id, qry_person_id, inc_entry_li,
|
||||
enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl
|
||||
return await _refresh_journal_li_background({
|
||||
api_cfg,
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
qry_person_id,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
|
||||
async function _refresh_journal_li_background({ api_cfg, for_obj_type, for_obj_id, qry_person_id, inc_entry_li, enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl }: any) {
|
||||
async function _refresh_journal_li_background({
|
||||
api_cfg,
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
qry_person_id,
|
||||
inc_entry_li,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl
|
||||
}: any) {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return [];
|
||||
|
||||
|
||||
let promise;
|
||||
if (qry_person_id) {
|
||||
const search_query: any = { and: [{ field: 'person_id_random', op: 'eq', value: qry_person_id }] };
|
||||
if (for_obj_id) search_query.and.push({ field: `${for_obj_type}_id_random`, op: 'eq', value: for_obj_id });
|
||||
if (enabled === 'enabled') search_query.and.push({ field: 'enable', op: 'eq', value: true });
|
||||
if (hidden === 'hidden') search_query.and.push({ field: 'hide', op: 'eq', value: true });
|
||||
const search_query: any = {
|
||||
and: [{ field: 'person_id_random', op: 'eq', value: qry_person_id }]
|
||||
};
|
||||
if (for_obj_id)
|
||||
search_query.and.push({
|
||||
field: `${for_obj_type}_id_random`,
|
||||
op: 'eq',
|
||||
value: for_obj_id
|
||||
});
|
||||
if (enabled === 'enabled')
|
||||
search_query.and.push({ field: 'enable', op: 'eq', value: true });
|
||||
if (hidden === 'hidden')
|
||||
search_query.and.push({ field: 'hide', op: 'eq', value: true });
|
||||
|
||||
promise = api.search_ae_obj_v3({ api_cfg, obj_type: 'journal', search_query, order_by_li, limit, offset, log_lvl });
|
||||
promise = api.search_ae_obj_v3({
|
||||
api_cfg,
|
||||
obj_type: 'journal',
|
||||
search_query,
|
||||
order_by_li,
|
||||
limit,
|
||||
offset,
|
||||
log_lvl
|
||||
});
|
||||
} else {
|
||||
promise = api.get_ae_obj_li_v3({ api_cfg, obj_type: 'journal', for_obj_type, for_obj_id, enabled, hidden, limit, offset, order_by_li, log_lvl });
|
||||
promise = api.get_ae_obj_li_v3({
|
||||
api_cfg,
|
||||
obj_type: 'journal',
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await promise;
|
||||
if (results) {
|
||||
if (try_cache) {
|
||||
const processed = await process_ae_obj__journal_props({ obj_li: results, log_lvl });
|
||||
await db_save_ae_obj_li__ae_obj({ db_instance: db_journals, table_name: 'journal', obj_li: processed, properties_to_save, log_lvl });
|
||||
const processed = await process_ae_obj__journal_props({
|
||||
obj_li: results,
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
if (inc_entry_li) {
|
||||
for (const journal of results) {
|
||||
load_ae_obj_li__journal_entry({
|
||||
api_cfg, for_obj_type: 'journal', for_obj_id: journal.journal_id_random,
|
||||
enabled, hidden, limit, offset, order_by_li, params, try_cache, log_lvl: 0
|
||||
api_cfg,
|
||||
for_obj_type: 'journal',
|
||||
for_obj_id: journal.journal_id_random,
|
||||
enabled,
|
||||
hidden,
|
||||
limit,
|
||||
offset,
|
||||
order_by_li,
|
||||
params,
|
||||
try_cache,
|
||||
log_lvl: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -216,11 +379,15 @@ export async function create_ae_obj__journal({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_Journal | null> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** create_ae_obj__journal() *** account_id=${account_id}`);
|
||||
console.log(
|
||||
`*** create_ae_obj__journal() *** account_id=${account_id}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!account_id) {
|
||||
console.log(`ERROR: Journals - Journal - account_id required to create`);
|
||||
console.log(
|
||||
`ERROR: Journals - Journal - account_id required to create`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -239,10 +406,11 @@ export async function create_ae_obj__journal({
|
||||
if (journal_obj_create_result) {
|
||||
if (try_cache) {
|
||||
// Process the results first
|
||||
const processed_obj_li = await process_ae_obj__journal_props({
|
||||
obj_li: [journal_obj_create_result],
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_props({
|
||||
obj_li: [journal_obj_create_result],
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
@@ -284,7 +452,9 @@ export async function delete_ae_obj_id__journal({
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** delete_ae_obj_id__journal() *** journal_id=${journal_id}`);
|
||||
console.log(
|
||||
`*** delete_ae_obj_id__journal() *** journal_id=${journal_id}`
|
||||
);
|
||||
}
|
||||
|
||||
ae_promises.delete__journal_obj = await api
|
||||
@@ -302,7 +472,9 @@ export async function delete_ae_obj_id__journal({
|
||||
.finally(async function () {
|
||||
if (try_cache) {
|
||||
if (log_lvl) {
|
||||
console.log(`Attempting to remove IDB entry for journal_id=${journal_id}`);
|
||||
console.log(
|
||||
`Attempting to remove IDB entry for journal_id=${journal_id}`
|
||||
);
|
||||
}
|
||||
await db_journals.journal.delete(journal_id);
|
||||
}
|
||||
@@ -328,7 +500,10 @@ export async function update_ae_obj__journal({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_Journal | null> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** update_ae_obj__journal() *** journal_id=${journal_id}`, data_kv);
|
||||
console.log(
|
||||
`*** update_ae_obj__journal() *** journal_id=${journal_id}`,
|
||||
data_kv
|
||||
);
|
||||
}
|
||||
|
||||
// Perform the API update
|
||||
@@ -401,7 +576,9 @@ export async function qry__journal({
|
||||
hidden?: 'hidden' | 'all' | 'not_hidden' | undefined; // all, hidden, not_hidden
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[];
|
||||
order_by_li?:
|
||||
| Record<string, 'ASC' | 'DESC'>
|
||||
| Record<string, 'ASC' | 'DESC'>[];
|
||||
params?: any;
|
||||
try_cache?: boolean;
|
||||
log_lvl?: number;
|
||||
@@ -417,17 +594,29 @@ export async function qry__journal({
|
||||
if (qry_files === true) {
|
||||
search_query.and.push({ field: 'file_count_all', op: 'gt', value: 0 });
|
||||
} else if (qry_files === false) {
|
||||
search_query.and.push({ field: 'file_count_all', op: 'is', value: null });
|
||||
search_query.and.push({
|
||||
field: 'file_count_all',
|
||||
op: 'is',
|
||||
value: null
|
||||
});
|
||||
}
|
||||
|
||||
if (qry_start_datetime) {
|
||||
search_query.and.push({ field: 'start_datetime', op: 'gt', value: qry_start_datetime });
|
||||
search_query.and.push({
|
||||
field: 'start_datetime',
|
||||
op: 'gt',
|
||||
value: qry_start_datetime
|
||||
});
|
||||
}
|
||||
|
||||
// Add for_obj_id context (Account ID)
|
||||
if (journal_id) {
|
||||
// Assuming journal_id here is actually the account_id as per original usage context
|
||||
search_query.and.push({ field: 'account_id_random', op: 'eq', value: journal_id });
|
||||
search_query.and.push({
|
||||
field: 'account_id_random',
|
||||
op: 'eq',
|
||||
value: journal_id
|
||||
});
|
||||
}
|
||||
|
||||
// Add enabled/hidden filters
|
||||
@@ -457,10 +646,11 @@ export async function qry__journal({
|
||||
if (journal_obj_li_get_result) {
|
||||
if (try_cache) {
|
||||
// Process the results first
|
||||
const processed_obj_li = await process_ae_obj__journal_props({
|
||||
obj_li: journal_obj_li_get_result,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_props({
|
||||
obj_li: journal_obj_li_get_result,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
@@ -486,7 +676,10 @@ export async function qry__journal({
|
||||
});
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('ae_promises.load__journal_obj_li:', ae_promises.load__journal_obj_li);
|
||||
console.log(
|
||||
'ae_promises.load__journal_obj_li:',
|
||||
ae_promises.load__journal_obj_li
|
||||
);
|
||||
}
|
||||
return ae_promises.load__journal_obj_li;
|
||||
}
|
||||
@@ -624,12 +817,16 @@ async function _process_generic_props<T extends Record<string, any>>({
|
||||
const updated = processed_obj.updated_on ?? processed_obj.created_on;
|
||||
const name = processed_obj.name ?? '';
|
||||
|
||||
(processed_obj as any).tmp_sort_1 = `${group}_${priority}_${sort}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_2 = `${group}_${priority}_${sort}_${name}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_1 =
|
||||
`${group}_${priority}_${sort}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_2 =
|
||||
`${group}_${priority}_${sort}_${name}_${updated}`;
|
||||
|
||||
// --- Specific Transformations ---
|
||||
if (specific_processor) {
|
||||
processed_obj = await Promise.resolve(specific_processor(processed_obj));
|
||||
processed_obj = await Promise.resolve(
|
||||
specific_processor(processed_obj)
|
||||
);
|
||||
}
|
||||
|
||||
processed_obj_li.push(processed_obj as T);
|
||||
@@ -656,7 +853,8 @@ export async function process_ae_obj__journal_props({
|
||||
/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,
|
||||
''
|
||||
);
|
||||
obj.description_md_html = (await marked.parse(description_cleaned ?? '')) ?? null;
|
||||
obj.description_md_html =
|
||||
(await marked.parse(description_cleaned ?? '')) ?? null;
|
||||
|
||||
obj.cfg_json = obj.cfg_json ?? {};
|
||||
obj.data_json = obj.data_json ?? {};
|
||||
@@ -669,4 +867,4 @@ export async function process_ae_obj__journal_props({
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ export async function load_ae_obj_id__journal_entry({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_JournalEntry | null> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** load_ae_obj_id__journal_entry() *** journal_entry_id=${journal_entry_id}`);
|
||||
console.log(
|
||||
`*** load_ae_obj_id__journal_entry() *** journal_entry_id=${journal_entry_id}`
|
||||
);
|
||||
}
|
||||
|
||||
ae_promises.load__journal_entry_obj = await api
|
||||
@@ -36,10 +38,11 @@ export async function load_ae_obj_id__journal_entry({
|
||||
if (journal_entry_obj_get_result) {
|
||||
if (try_cache) {
|
||||
// Process the results first
|
||||
const processed_obj_li = await process_ae_obj__journal_entry_props({
|
||||
obj_li: [journal_entry_obj_get_result],
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_entry_props({
|
||||
obj_li: [journal_entry_obj_get_result],
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
@@ -138,11 +141,15 @@ export async function load_ae_obj_li__journal_entry({
|
||||
if (journal_entry_obj_li_get_result) {
|
||||
if (try_cache) {
|
||||
// Process the results first
|
||||
const processed_obj_li = await process_ae_obj__journal_entry_props({
|
||||
obj_li: journal_entry_obj_li_get_result,
|
||||
journal_id: for_obj_type === 'journal' ? for_obj_id : undefined,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_entry_props({
|
||||
obj_li: journal_entry_obj_li_get_result,
|
||||
journal_id:
|
||||
for_obj_type === 'journal'
|
||||
? for_obj_id
|
||||
: undefined,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
@@ -197,7 +204,9 @@ export async function create_ae_obj__journal_entry({
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_JournalEntry | null> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** create_ae_obj__journal_entry() *** journal_id=${journal_id}`);
|
||||
console.log(
|
||||
`*** create_ae_obj__journal_entry() *** journal_id=${journal_id}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!journal_id) {
|
||||
@@ -219,11 +228,12 @@ export async function create_ae_obj__journal_entry({
|
||||
if (journal_entry_obj_create_result) {
|
||||
if (try_cache) {
|
||||
// Process the results first
|
||||
const processed_obj_li = await process_ae_obj__journal_entry_props({
|
||||
obj_li: [journal_entry_obj_create_result],
|
||||
journal_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_entry_props({
|
||||
obj_li: [journal_entry_obj_create_result],
|
||||
journal_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
@@ -342,36 +352,64 @@ export async function qry__journal_entry({
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** qry__journal_entry() *** journal_id=${journal_id} person_id=${person_id}`);
|
||||
console.log(
|
||||
`*** qry__journal_entry() *** journal_id=${journal_id} person_id=${person_id}`
|
||||
);
|
||||
}
|
||||
|
||||
const search_query: any = { and: [] };
|
||||
|
||||
if (qry_str) {
|
||||
// Using 'like' with wildcards to ensure compatibility
|
||||
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str.trim()}%` });
|
||||
params['lk_qry'] = { 'default_qry_str': qry_str.trim() };
|
||||
search_query.and.push({
|
||||
field: 'default_qry_str',
|
||||
op: 'like',
|
||||
value: `%${qry_str.trim()}%`
|
||||
});
|
||||
params['lk_qry'] = { default_qry_str: qry_str.trim() };
|
||||
}
|
||||
|
||||
if (qry_category_code) {
|
||||
search_query.and.push({ field: 'category_code', op: 'eq', value: qry_category_code });
|
||||
search_query.and.push({
|
||||
field: 'category_code',
|
||||
op: 'eq',
|
||||
value: qry_category_code
|
||||
});
|
||||
}
|
||||
|
||||
if (qry_created_on) {
|
||||
search_query.and.push({ field: 'created_on', op: 'gt', value: qry_created_on });
|
||||
search_query.and.push({
|
||||
field: 'created_on',
|
||||
op: 'gt',
|
||||
value: qry_created_on
|
||||
});
|
||||
}
|
||||
|
||||
if (qry_priority) {
|
||||
search_query.and.push({ field: 'priority', op: 'eq', value: qry_priority });
|
||||
search_query.and.push({
|
||||
field: 'priority',
|
||||
op: 'eq',
|
||||
value: qry_priority
|
||||
});
|
||||
}
|
||||
|
||||
// Context scoping: Prefer journal_id if provided, otherwise fallback to person_id (global search)
|
||||
if (journal_id) {
|
||||
search_query.and.push({ field: 'journal_id_random', op: 'eq', value: journal_id });
|
||||
search_query.and.push({
|
||||
field: 'journal_id_random',
|
||||
op: 'eq',
|
||||
value: journal_id
|
||||
});
|
||||
} else if (person_id) {
|
||||
search_query.and.push({ field: 'person_id_random', op: 'eq', value: person_id });
|
||||
search_query.and.push({
|
||||
field: 'person_id_random',
|
||||
op: 'eq',
|
||||
value: person_id
|
||||
});
|
||||
} else {
|
||||
console.warn('qry__journal_entry: No journal_id or person_id provided. Search might be too broad.');
|
||||
console.warn(
|
||||
'qry__journal_entry: No journal_id or person_id provided. Search might be too broad.'
|
||||
);
|
||||
}
|
||||
|
||||
// Add enabled/hidden filters
|
||||
@@ -413,11 +451,12 @@ export async function qry__journal_entry({
|
||||
|
||||
if (valid_result_li && valid_result_li.length > 0) {
|
||||
if (try_cache) {
|
||||
const processed_obj_li = await process_ae_obj__journal_entry_props({
|
||||
obj_li: valid_result_li,
|
||||
journal_id,
|
||||
log_lvl
|
||||
});
|
||||
const processed_obj_li =
|
||||
await process_ae_obj__journal_entry_props({
|
||||
obj_li: valid_result_li,
|
||||
journal_id,
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
@@ -907,12 +946,16 @@ async function _process_generic_props<T extends Record<string, any>>({
|
||||
const updated = processed_obj.updated_on ?? processed_obj.created_on;
|
||||
const name = processed_obj.name ?? '';
|
||||
|
||||
(processed_obj as any).tmp_sort_1 = `${group}_${priority}_${sort}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_2 = `${group}_${priority}_${sort}_${name}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_1 =
|
||||
`${group}_${priority}_${sort}_${updated}`;
|
||||
(processed_obj as any).tmp_sort_2 =
|
||||
`${group}_${priority}_${sort}_${name}_${updated}`;
|
||||
|
||||
// --- Specific Transformations ---
|
||||
if (specific_processor) {
|
||||
processed_obj = await Promise.resolve(specific_processor(processed_obj));
|
||||
processed_obj = await Promise.resolve(
|
||||
specific_processor(processed_obj)
|
||||
);
|
||||
}
|
||||
|
||||
processed_obj_li.push(processed_obj as T);
|
||||
@@ -952,8 +995,12 @@ export async function process_ae_obj__journal_entry_props({
|
||||
content_cleaned = null;
|
||||
content_md_html = null;
|
||||
} else {
|
||||
content_cleaned = content.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, '');
|
||||
content_md_html = (await marked.parse(content_cleaned ?? '')) ?? null;
|
||||
content_cleaned = content.replace(
|
||||
/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,
|
||||
''
|
||||
);
|
||||
content_md_html =
|
||||
(await marked.parse(content_cleaned ?? '')) ?? null;
|
||||
}
|
||||
obj.content = content;
|
||||
obj.content_md_html = content_md_html;
|
||||
@@ -968,8 +1015,12 @@ export async function process_ae_obj__journal_entry_props({
|
||||
history_cleaned = null;
|
||||
history_md_html = null;
|
||||
} else {
|
||||
history_cleaned = history.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, '');
|
||||
history_md_html = (await marked.parse(history_cleaned ?? '')) ?? null;
|
||||
history_cleaned = history.replace(
|
||||
/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,
|
||||
''
|
||||
);
|
||||
history_md_html =
|
||||
(await marked.parse(history_cleaned ?? '')) ?? null;
|
||||
}
|
||||
obj.history = history;
|
||||
obj.history_md_html = history_md_html;
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface DecryptionResult {
|
||||
|
||||
/**
|
||||
* Decrypts a journal entry's content and history using the provided or stored passcode.
|
||||
*
|
||||
*
|
||||
* @param entry The journal entry object to decrypt.
|
||||
* @param journal The parent journal object (provides base passcode and private_passcode).
|
||||
* @param typed_passcode Optional: A user-entered passcode override.
|
||||
@@ -24,11 +24,10 @@ export async function decrypt_journal_entry(
|
||||
journal: ae_Journal,
|
||||
typed_passcode?: string
|
||||
): Promise<DecryptionResult> {
|
||||
|
||||
// Safety check: if not encrypted, return as-is
|
||||
if (!entry.content_encrypted) {
|
||||
return {
|
||||
success: true,
|
||||
return {
|
||||
success: true,
|
||||
content: entry.content ?? '',
|
||||
history: entry.history ?? ''
|
||||
};
|
||||
@@ -37,36 +36,46 @@ export async function decrypt_journal_entry(
|
||||
// Determine which key to use
|
||||
let journal_key = typed_passcode;
|
||||
let key_source = 'user_typed';
|
||||
|
||||
|
||||
// If no override, try the private passcode stored on the journal object
|
||||
if (!journal_key?.length) {
|
||||
journal_key = journal.private_passcode;
|
||||
key_source = 'journal_private_passcode';
|
||||
}
|
||||
|
||||
|
||||
if (!journal_key) {
|
||||
console.warn('decrypt_journal_entry: No key available. Source:', key_source);
|
||||
return {
|
||||
success: false,
|
||||
error: 'No passcode provided or available for decryption.'
|
||||
console.warn(
|
||||
'decrypt_journal_entry: No key available. Source:',
|
||||
key_source
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'No passcode provided or available for decryption.'
|
||||
};
|
||||
}
|
||||
|
||||
// Aether standard: combine the journal's public passcode with the private key
|
||||
const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`;
|
||||
|
||||
console.log(`decrypt_journal_entry: Attempting decryption. Source: ${key_source}`);
|
||||
|
||||
console.log(
|
||||
`decrypt_journal_entry: Attempting decryption. Source: ${key_source}`
|
||||
);
|
||||
// console.log(`decrypt_journal_entry: Key: ${decrypt_key}`); // Log ONLY for deep debugging
|
||||
|
||||
try {
|
||||
// Decrypt Primary Content
|
||||
const result = await ae_util.decrypt_wrapper(entry.content_encrypted, decrypt_key);
|
||||
const result = await ae_util.decrypt_wrapper(
|
||||
entry.content_encrypted,
|
||||
decrypt_key
|
||||
);
|
||||
|
||||
if (result === false) {
|
||||
console.error('decrypt_journal_entry: Decryption wrapper returned false.');
|
||||
return {
|
||||
success: false,
|
||||
error: 'Decryption failed. Incorrect passcode or corrupted data.'
|
||||
console.error(
|
||||
'decrypt_journal_entry: Decryption wrapper returned false.'
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Decryption failed. Incorrect passcode or corrupted data.'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,24 +84,28 @@ export async function decrypt_journal_entry(
|
||||
|
||||
// Decrypt History (if it exists)
|
||||
if (entry.history_encrypted) {
|
||||
const h_res = await ae_util.decrypt_wrapper(entry.history_encrypted, decrypt_key);
|
||||
const h_res = await ae_util.decrypt_wrapper(
|
||||
entry.history_encrypted,
|
||||
decrypt_key
|
||||
);
|
||||
if (h_res !== false) {
|
||||
decrypted_history = typeof h_res === 'string' ? h_res : '';
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`decrypt_journal_entry: SUCCESS. Source: ${key_source}, Content length: ${decrypted_text.length}. Preview: ${decrypted_text.substring(0, 30).replace(/\n/g, ' ')}...`);
|
||||
return {
|
||||
success: true,
|
||||
console.log(
|
||||
`decrypt_journal_entry: SUCCESS. Source: ${key_source}, Content length: ${decrypted_text.length}. Preview: ${decrypted_text.substring(0, 30).replace(/\n/g, ' ')}...`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
content: decrypted_text,
|
||||
history: decrypted_history
|
||||
};
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('decrypt_journal_entry error:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: `System error during decryption: ${err.message}`
|
||||
return {
|
||||
success: false,
|
||||
error: `System error during decryption: ${err.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
/**
|
||||
* Wraps the current selection in CodeMirror with the given prefix and suffix.
|
||||
*/
|
||||
export function wrapSelection(view: any, prefix: string, suffix: string = prefix) {
|
||||
export function wrapSelection(
|
||||
view: any,
|
||||
prefix: string,
|
||||
suffix: string = prefix
|
||||
) {
|
||||
if (!view || view.updateInProgress) return;
|
||||
|
||||
|
||||
const { state } = view;
|
||||
view.dispatch(state.changeByRange((range: any) => {
|
||||
return {
|
||||
changes: [
|
||||
{from: range.from, insert: prefix},
|
||||
{from: range.to, insert: suffix}
|
||||
],
|
||||
range: range.constructor.range(range.from + prefix.length, range.to + prefix.length)
|
||||
};
|
||||
}));
|
||||
view.dispatch(
|
||||
state.changeByRange((range: any) => {
|
||||
return {
|
||||
changes: [
|
||||
{ from: range.from, insert: prefix },
|
||||
{ from: range.to, insert: suffix }
|
||||
],
|
||||
range: range.constructor.range(
|
||||
range.from + prefix.length,
|
||||
range.to + prefix.length
|
||||
)
|
||||
};
|
||||
})
|
||||
);
|
||||
view.focus();
|
||||
}
|
||||
|
||||
@@ -22,32 +31,49 @@ export function wrapSelection(view: any, prefix: string, suffix: string = prefix
|
||||
*/
|
||||
export function toggleLinePrefix(view: any, prefix: string) {
|
||||
if (!view || view.updateInProgress) return;
|
||||
|
||||
|
||||
const { state } = view;
|
||||
view.dispatch(state.changeByRange((range: any) => {
|
||||
const lines = [];
|
||||
for (let pos = range.from; pos <= range.to; ) {
|
||||
const line = state.doc.lineAt(pos);
|
||||
lines.push(line);
|
||||
pos = line.to + 1;
|
||||
}
|
||||
|
||||
const isAlreadyPrefixed = lines.every(l => l.text.startsWith(prefix));
|
||||
const lineChanges = lines.map(l => {
|
||||
if (isAlreadyPrefixed) {
|
||||
return { from: l.from, to: l.from + prefix.length, insert: '' };
|
||||
} else {
|
||||
return { from: l.from, insert: prefix };
|
||||
view.dispatch(
|
||||
state.changeByRange((range: any) => {
|
||||
const lines = [];
|
||||
for (let pos = range.from; pos <= range.to; ) {
|
||||
const line = state.doc.lineAt(pos);
|
||||
lines.push(line);
|
||||
pos = line.to + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const newFrom = range.from + (isAlreadyPrefixed ? -prefix.length : prefix.length);
|
||||
const newTo = range.to + (isAlreadyPrefixed ? (-prefix.length * lines.length) : (prefix.length * lines.length));
|
||||
const isAlreadyPrefixed = lines.every((l) =>
|
||||
l.text.startsWith(prefix)
|
||||
);
|
||||
const lineChanges = lines.map((l) => {
|
||||
if (isAlreadyPrefixed) {
|
||||
return {
|
||||
from: l.from,
|
||||
to: l.from + prefix.length,
|
||||
insert: ''
|
||||
};
|
||||
} else {
|
||||
return { from: l.from, insert: prefix };
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
changes: lineChanges,
|
||||
range: range.constructor.range(newFrom, Math.max(newFrom, newTo))
|
||||
};
|
||||
}));
|
||||
const newFrom =
|
||||
range.from +
|
||||
(isAlreadyPrefixed ? -prefix.length : prefix.length);
|
||||
const newTo =
|
||||
range.to +
|
||||
(isAlreadyPrefixed
|
||||
? -prefix.length * lines.length
|
||||
: prefix.length * lines.length);
|
||||
|
||||
return {
|
||||
changes: lineChanges,
|
||||
range: range.constructor.range(
|
||||
newFrom,
|
||||
Math.max(newFrom, newTo)
|
||||
)
|
||||
};
|
||||
})
|
||||
);
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,17 @@ export const template_standard_markdown: ExportTemplate = {
|
||||
description: 'Basic Markdown with title, date, and content.',
|
||||
extension: 'md',
|
||||
formatter: (entries) => {
|
||||
return entries.map(entry => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long');
|
||||
const title = entry.name || dateStr;
|
||||
const header = `# ${title}\n*${dateStr}*\n\n`;
|
||||
return `${header}${entry.content || ''}\n\n---\n`;
|
||||
}).join('\n');
|
||||
return entries
|
||||
.map((entry) => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'datetime_12_long'
|
||||
);
|
||||
const title = entry.name || dateStr;
|
||||
const header = `# ${title}\n*${dateStr}*\n\n`;
|
||||
return `${header}${entry.content || ''}\n\n---\n`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,12 +55,17 @@ export const template_personal_log: ExportTemplate = {
|
||||
return dateA.localeCompare(dateB);
|
||||
});
|
||||
|
||||
return sorted.map(entry => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'date_iso');
|
||||
const title = entry.name || '';
|
||||
const header = `## ${dateStr}${title ? ' - ' + title : ''}\n\n`;
|
||||
return `${header}${entry.content || ''}\n\n`;
|
||||
}).join('\n');
|
||||
return sorted
|
||||
.map((entry) => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'date_iso'
|
||||
);
|
||||
const title = entry.name || '';
|
||||
const header = `## ${dateStr}${title ? ' - ' + title : ''}\n\n`;
|
||||
return `${header}${entry.content || ''}\n\n`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,19 +79,24 @@ export const template_amazon_vine: ExportTemplate = {
|
||||
description: 'Optimized for product reviews.',
|
||||
extension: 'md',
|
||||
formatter: (entries) => {
|
||||
return entries.map(entry => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'date_iso');
|
||||
// Try to find a product name in the title or content
|
||||
const productName = entry.name || 'Unknown Product';
|
||||
|
||||
// Look for product link in content_json or tags if available,
|
||||
// otherwise just output content
|
||||
let output = `## ${productName}\n`;
|
||||
output += `*Date: ${dateStr}*\n\n`;
|
||||
output += `${entry.content || ''}\n\n`;
|
||||
output += `---`;
|
||||
return output;
|
||||
}).join('\n\n');
|
||||
return entries
|
||||
.map((entry) => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'date_iso'
|
||||
);
|
||||
// Try to find a product name in the title or content
|
||||
const productName = entry.name || 'Unknown Product';
|
||||
|
||||
// Look for product link in content_json or tags if available,
|
||||
// otherwise just output content
|
||||
let output = `## ${productName}\n`;
|
||||
output += `*Date: ${dateStr}*\n\n`;
|
||||
output += `${entry.content || ''}\n\n`;
|
||||
output += `---`;
|
||||
return output;
|
||||
})
|
||||
.join('\n\n');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,10 +109,14 @@ export const template_standard_html: ExportTemplate = {
|
||||
description: 'Semantic HTML5 articles.',
|
||||
extension: 'html',
|
||||
formatter: (entries) => {
|
||||
const body = entries.map(entry => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long');
|
||||
const title = entry.name || dateStr;
|
||||
return `
|
||||
const body = entries
|
||||
.map((entry) => {
|
||||
const dateStr = ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'datetime_12_long'
|
||||
);
|
||||
const title = entry.name || dateStr;
|
||||
return `
|
||||
<article class="journal-entry" id="entry-${entry.journal_entry_id}">
|
||||
<header>
|
||||
<h1>${title}</h1>
|
||||
@@ -108,7 +127,8 @@ export const template_standard_html: ExportTemplate = {
|
||||
</div>
|
||||
<hr/>
|
||||
</article>`;
|
||||
}).join('\n');
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -13,7 +13,8 @@ const export_obj = {
|
||||
load_ae_obj_id__journal_entry: journal_entry.load_ae_obj_id__journal_entry,
|
||||
load_ae_obj_li__journal_entry: journal_entry.load_ae_obj_li__journal_entry,
|
||||
create_ae_obj__journal_entry: journal_entry.create_ae_obj__journal_entry,
|
||||
delete_ae_obj_id__journal_entry: journal_entry.delete_ae_obj_id__journal_entry,
|
||||
delete_ae_obj_id__journal_entry:
|
||||
journal_entry.delete_ae_obj_id__journal_entry,
|
||||
update_ae_obj__journal_entry: journal_entry.update_ae_obj__journal_entry,
|
||||
qry__journal_entry: journal_entry.qry__journal_entry
|
||||
// db_save_ae_obj_li__journal_entry: journal_entry.db_save_ae_obj_li__journal_entry,
|
||||
|
||||
@@ -19,7 +19,10 @@ export interface AeJournalEntryInput {
|
||||
* - First line is title (if it looks like a title).
|
||||
* - Rest is content.
|
||||
*/
|
||||
export async function parse_standard_note(file: File, text: string): Promise<AeJournalEntryInput[]> {
|
||||
export async function parse_standard_note(
|
||||
file: File,
|
||||
text: string
|
||||
): Promise<AeJournalEntryInput[]> {
|
||||
const lines = text.split('\n');
|
||||
let name = file.name.replace(/\.md$/i, '').replace(/\.txt$/i, '');
|
||||
let content = text;
|
||||
@@ -29,11 +32,15 @@ export async function parse_standard_note(file: File, text: string): Promise<AeJ
|
||||
if (lines.length > 0 && lines[0].startsWith('# ')) {
|
||||
name = lines[0].substring(2).trim();
|
||||
content = lines.slice(1).join('\n').trim();
|
||||
} else if (lines.length > 0 && lines[0].trim().length > 0 && lines[0].trim().length < 60) {
|
||||
} else if (
|
||||
lines.length > 0 &&
|
||||
lines[0].trim().length > 0 &&
|
||||
lines[0].trim().length < 60
|
||||
) {
|
||||
// First line is short, treat as title if it doesn't look like frontmatter
|
||||
if (lines[0].trim() !== '---') {
|
||||
name = lines[0].trim();
|
||||
content = lines.slice(1).join('\n').trim();
|
||||
name = lines[0].trim();
|
||||
content = lines.slice(1).join('\n').trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,18 +49,28 @@ export async function parse_standard_note(file: File, text: string): Promise<AeJ
|
||||
const endFrontmatter = lines.indexOf('---', 1);
|
||||
if (endFrontmatter > -1) {
|
||||
const frontmatter = lines.slice(1, endFrontmatter);
|
||||
content = lines.slice(endFrontmatter + 1).join('\n').trim();
|
||||
|
||||
content = lines
|
||||
.slice(endFrontmatter + 1)
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
// Extract tags or title from frontmatter (very basic parsing)
|
||||
frontmatter.forEach(line => {
|
||||
if (line.startsWith('title:')) name = line.substring(6).trim().replace(/^['"]|['"]$/g, '');
|
||||
frontmatter.forEach((line) => {
|
||||
if (line.startsWith('title:'))
|
||||
name = line
|
||||
.substring(6)
|
||||
.trim()
|
||||
.replace(/^['"]|['"]$/g, '');
|
||||
if (line.startsWith('tags:')) {
|
||||
// This is brittle, assumes inline tags like [a, b] or comma separated
|
||||
const tagPart = line.substring(5).trim();
|
||||
if (tagPart.startsWith('[') && tagPart.endsWith(']')) {
|
||||
tagPart.substring(1, tagPart.length - 1).split(',').forEach(t => tags.push(t.trim()));
|
||||
tagPart
|
||||
.substring(1, tagPart.length - 1)
|
||||
.split(',')
|
||||
.forEach((t) => tags.push(t.trim()));
|
||||
} else {
|
||||
tagPart.split(',').forEach(t => tags.push(t.trim()));
|
||||
tagPart.split(',').forEach((t) => tags.push(t.trim()));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -62,31 +79,36 @@ export async function parse_standard_note(file: File, text: string): Promise<AeJ
|
||||
|
||||
const lastModified = new Date(file.lastModified).toISOString();
|
||||
|
||||
return [{
|
||||
name,
|
||||
content,
|
||||
tags,
|
||||
updated_on: lastModified,
|
||||
created_on: lastModified, // We don't really know creation time from File object usually
|
||||
original_filename: file.name,
|
||||
type_code: 'note'
|
||||
}];
|
||||
return [
|
||||
{
|
||||
name,
|
||||
content,
|
||||
tags,
|
||||
updated_on: lastModified,
|
||||
created_on: lastModified, // We don't really know creation time from File object usually
|
||||
original_filename: file.name,
|
||||
type_code: 'note'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Personal Log Parser
|
||||
* - Splits file by dates: `## YYYY-MM-DD`
|
||||
*/
|
||||
export async function parse_personal_log(file: File, text: string): Promise<AeJournalEntryInput[]> {
|
||||
export async function parse_personal_log(
|
||||
file: File,
|
||||
text: string
|
||||
): Promise<AeJournalEntryInput[]> {
|
||||
const entries: AeJournalEntryInput[] = [];
|
||||
const dateRegex = /^##\s+(\d{4}-\d{2}-\d{2})(.*)$/;
|
||||
|
||||
|
||||
const lines = text.split('\n');
|
||||
let currentEntry: Partial<AeJournalEntryInput> | null = null;
|
||||
let currentContent: string[] = [];
|
||||
|
||||
|
||||
// Check if the whole file is just one entry (no date headers)
|
||||
if (!lines.some(l => dateRegex.test(l))) {
|
||||
if (!lines.some((l) => dateRegex.test(l))) {
|
||||
return parse_standard_note(file, text);
|
||||
}
|
||||
|
||||
@@ -104,19 +126,21 @@ export async function parse_personal_log(file: File, text: string): Promise<AeJo
|
||||
// Start new entry
|
||||
const dateStr = match[1];
|
||||
const extraTitle = match[2].trim();
|
||||
|
||||
|
||||
currentEntry = {
|
||||
name: extraTitle ? `${dateStr} - ${extraTitle}` : `${fileBaseName} - ${dateStr}`,
|
||||
name: extraTitle
|
||||
? `${dateStr} - ${extraTitle}`
|
||||
: `${fileBaseName} - ${dateStr}`,
|
||||
created_on: `${dateStr}T12:00:00`, // Noon on that day
|
||||
updated_on: new Date(file.lastModified).toISOString(),
|
||||
tags: ['log'],
|
||||
type_code: 'log',
|
||||
original_filename: file.name,
|
||||
original_filename: file.name
|
||||
// Reconstruct the header as part of content? Or just skip it?
|
||||
// Python parser added it back: `## {date_str}\n\n{body}`
|
||||
// Let's add it back for context.
|
||||
};
|
||||
currentContent = [`## ${dateStr} ${extraTitle}`];
|
||||
currentContent = [`## ${dateStr} ${extraTitle}`];
|
||||
} else {
|
||||
if (currentEntry) {
|
||||
currentContent.push(line);
|
||||
@@ -141,25 +165,30 @@ export async function parse_personal_log(file: File, text: string): Promise<AeJo
|
||||
* - Splits by `## Product Name`
|
||||
* - Looks for URL and `### Review Title`
|
||||
*/
|
||||
export async function parse_amazon_vine(file: File, text: string): Promise<AeJournalEntryInput[]> {
|
||||
export async function parse_amazon_vine(
|
||||
file: File,
|
||||
text: string
|
||||
): Promise<AeJournalEntryInput[]> {
|
||||
// Split by `\n## ` but we need to keep the delimiter or reconstruct
|
||||
// JS split doesn't keep delimiter nicely unless captured.
|
||||
// Let's iterate lines.
|
||||
const entries: AeJournalEntryInput[] = [];
|
||||
const productHeaderRegex = /^##\s+(.+)$/;
|
||||
|
||||
|
||||
const lines = text.split('\n');
|
||||
let currentEntry: any = null;
|
||||
let currentBody: string[] = [];
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(productHeaderRegex);
|
||||
if (match) {
|
||||
if (currentEntry) {
|
||||
entries.push(format_vine_entry(currentEntry, currentBody, file));
|
||||
}
|
||||
currentEntry = { productName: match[1].trim() };
|
||||
currentBody = [];
|
||||
if (currentEntry) {
|
||||
entries.push(
|
||||
format_vine_entry(currentEntry, currentBody, file)
|
||||
);
|
||||
}
|
||||
currentEntry = { productName: match[1].trim() };
|
||||
currentBody = [];
|
||||
} else {
|
||||
if (currentEntry) {
|
||||
currentBody.push(line);
|
||||
@@ -169,15 +198,19 @@ export async function parse_amazon_vine(file: File, text: string): Promise<AeJou
|
||||
if (currentEntry) {
|
||||
entries.push(format_vine_entry(currentEntry, currentBody, file));
|
||||
}
|
||||
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function format_vine_entry(entry: any, bodyLines: string[], file: File): AeJournalEntryInput {
|
||||
function format_vine_entry(
|
||||
entry: any,
|
||||
bodyLines: string[],
|
||||
file: File
|
||||
): AeJournalEntryInput {
|
||||
let url = '';
|
||||
let reviewTitle = '';
|
||||
const cleanBody: string[] = [];
|
||||
|
||||
|
||||
for (const line of bodyLines) {
|
||||
const trimmed = line.trim();
|
||||
if (!url && trimmed.startsWith('* http')) {
|
||||
@@ -190,12 +223,12 @@ function format_vine_entry(entry: any, bodyLines: string[], file: File): AeJourn
|
||||
}
|
||||
cleanBody.push(line);
|
||||
}
|
||||
|
||||
|
||||
let content = '';
|
||||
if (reviewTitle) content += `# ${reviewTitle}\n\n`;
|
||||
content += cleanBody.join('\n').trim();
|
||||
if (url) content += `\n\n**Product Link:** ${url}`;
|
||||
|
||||
|
||||
return {
|
||||
name: entry.productName,
|
||||
content: content,
|
||||
|
||||
@@ -38,7 +38,8 @@ const journals_local_data_struct: key_val = {
|
||||
llm__max_retries: 3, // Number of times to retry a failed LLM API call.
|
||||
llm__retry_delay_ms: 2000, // 2 seconds between retries.
|
||||
|
||||
llm__system_prompt: 'You are a helpful assistant that helps people find information.',
|
||||
llm__system_prompt:
|
||||
'You are a helpful assistant that helps people find information.',
|
||||
llm__max_tokens: 1024,
|
||||
llm__temperature: 0.7,
|
||||
llm__top_p: 1.0,
|
||||
@@ -57,8 +58,8 @@ const journals_local_data_struct: key_val = {
|
||||
qry__limit: 25,
|
||||
qry__offset: 0,
|
||||
qry__order_by_li: {
|
||||
// 'created_on': 'desc',
|
||||
// 'updated_on': 'desc',
|
||||
// 'created_on': 'desc',
|
||||
// 'updated_on': 'desc',
|
||||
},
|
||||
|
||||
type_code_li: [
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { ae_Journal, ae_JournalEntry } from '$lib/types/ae_types';
|
||||
export interface Journal extends ae_Journal {
|
||||
// Add any Dexie-specific or legacy local-only fields here if not in ae_Journal
|
||||
// Most fields are now in ae_Journal and ae_BaseObj
|
||||
|
||||
|
||||
// For backward compatibility with some views that expect these
|
||||
combined_passcode?: string;
|
||||
person__given_name?: string;
|
||||
@@ -29,7 +29,7 @@ export interface Journal extends ae_Journal {
|
||||
person__primary_email?: string;
|
||||
person__passcode?: string;
|
||||
person__kv_json?: string;
|
||||
|
||||
|
||||
journal_entry_kv?: key_val;
|
||||
journal_entry_li?: any[];
|
||||
journal_file_kv?: key_val;
|
||||
@@ -80,7 +80,7 @@ export interface Journal_Entry extends ae_JournalEntry {
|
||||
person__primary_email?: string;
|
||||
person__passcode?: string;
|
||||
person__kv_json?: string;
|
||||
|
||||
|
||||
journal_file_kv?: key_val;
|
||||
journal_file_li?: any[];
|
||||
}
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
|
||||
function scroll_container() {
|
||||
return (
|
||||
document.getElementById('ae_main_content') || document.documentElement || document.body
|
||||
document.getElementById('ae_main_content') ||
|
||||
document.documentElement ||
|
||||
document.body
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -124,7 +126,10 @@
|
||||
>
|
||||
<span class="justify-self-start">
|
||||
<!-- Be sure to explain what Æ (Aether) means in the title text or similar! -->
|
||||
<Satellite size="1.5em" class="mx-1 inline-block text-gray-500" />
|
||||
<Satellite
|
||||
size="1.5em"
|
||||
class="mx-1 inline-block text-gray-500"
|
||||
/>
|
||||
<abbr title="Aether - Journals Module"> Æ Journals </abbr>
|
||||
</span>
|
||||
<a
|
||||
@@ -136,7 +141,8 @@
|
||||
</a>
|
||||
<!-- <a href="/about" class="btn btn-sm">About</a> -->
|
||||
<!-- <a href="/settings" class="btn btn-sm">Settings</a> -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if ($ae_loc.edit_mode) {
|
||||
// Confirm before clearing
|
||||
@@ -249,7 +255,8 @@
|
||||
"
|
||||
>
|
||||
<!-- Scroll to top button -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
ae_btn_success_outlined
|
||||
|
||||
@@ -290,7 +297,8 @@
|
||||
|
||||
<!-- Scroll to the right button -->
|
||||
<!-- Temporarily hidden until I figure out a better way to do this -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class:hidden={1 == 1}
|
||||
class="
|
||||
ae_btn_success_outlined
|
||||
@@ -318,7 +326,8 @@
|
||||
</button>
|
||||
|
||||
<!-- Scroll to bottom button -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
ae_btn_success_outlined
|
||||
|
||||
@@ -392,9 +401,12 @@
|
||||
</footer>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center">
|
||||
<section
|
||||
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center"
|
||||
>
|
||||
<p class="text-center">
|
||||
You are not logged in as a user. You must be signed in to access the journals module.
|
||||
You are not logged in as a user. You must be signed in to access the
|
||||
journals module.
|
||||
</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -13,14 +13,18 @@ export async function load({ params, parent }) {
|
||||
|
||||
const account_id = parent_data.account_id;
|
||||
if (!account_id) {
|
||||
console.log(`journals +layout.ts: The account_id was not found in the parent_data!!!`);
|
||||
console.log(
|
||||
`journals +layout.ts: The account_id was not found in the parent_data!!!`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let ae_acct = parent_data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Journals +layout.ts: Account ${account_id} not found in parent data. Initializing ghost acct.`);
|
||||
console.warn(
|
||||
`ae Journals +layout.ts: Account ${account_id} not found in parent data. Initializing ghost acct.`
|
||||
);
|
||||
ae_acct = {
|
||||
api: parent_data.ae_api || {},
|
||||
slct: {
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
|
||||
// *** Icons
|
||||
import {
|
||||
BookPlus, SquareLibrary, Wrench,
|
||||
FileUp, Loader2, Sparkles
|
||||
BookPlus,
|
||||
SquareLibrary,
|
||||
Wrench,
|
||||
FileUp,
|
||||
Loader2,
|
||||
Sparkles
|
||||
} from 'lucide-svelte';
|
||||
|
||||
// *** Libraries & Stores
|
||||
@@ -68,7 +72,9 @@
|
||||
person_id_random: $ae_loc.person_id,
|
||||
name,
|
||||
type_code: type,
|
||||
cfg_json: { category_li: [{ code: '', name: 'Default' }] }
|
||||
cfg_json: {
|
||||
category_li: [{ code: '', name: 'Default' }]
|
||||
}
|
||||
}
|
||||
});
|
||||
if (results?.journal_id_random) {
|
||||
@@ -84,20 +90,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page_container flex flex-col gap-8 items-center w-full min-h-screen p-4 md:p-8">
|
||||
|
||||
<div
|
||||
class="page_container flex flex-col gap-8 items-center w-full min-h-screen p-4 md:p-8"
|
||||
>
|
||||
<!-- Header Section -->
|
||||
<header class="text-center space-y-2 max-w-3xl">
|
||||
<h1 class="text-4xl md:text-5xl font-black tracking-tight text-surface-900 dark:text-surface-100">
|
||||
<h1
|
||||
class="text-4xl md:text-5xl font-black tracking-tight text-surface-900 dark:text-surface-100"
|
||||
>
|
||||
<!-- <div class="p-3 bg-surface-500/10 rounded-2xl"> -->
|
||||
<SquareLibrary size="1em" class="text-primary-500 inline-block" />
|
||||
<SquareLibrary size="1em" class="text-primary-500 inline-block" />
|
||||
<!-- </div> -->
|
||||
Journals
|
||||
</h1>
|
||||
<p class="text-surface-600 dark:text-surface-400 font-medium">
|
||||
Managed by <span class="text-primary-500">{$ae_loc.account_name ?? 'Æ loading...'}</span>
|
||||
Managed by <span class="text-primary-500"
|
||||
>{$ae_loc.account_name ?? 'Æ loading...'}</span
|
||||
>
|
||||
{#if $ae_loc.person.given_name}
|
||||
• <span class="opacity-75">{$ae_loc.person.given_name}</span>
|
||||
• <span class="opacity-75"
|
||||
>{$ae_loc.person.given_name}</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</header>
|
||||
@@ -105,13 +118,17 @@
|
||||
<!-- Quick Add Integrated Section -->
|
||||
<section class="w-full max-w-2xl">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||
<div
|
||||
class="absolute -inset-1 bg-gradient-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"
|
||||
></div>
|
||||
<AeCompJournalEntryQuickAdd
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-center gap-2 text-xs opacity-50 font-bold uppercase tracking-widest">
|
||||
<div
|
||||
class="mt-2 flex items-center justify-center gap-2 text-xs opacity-50 font-bold uppercase tracking-widest"
|
||||
>
|
||||
<Sparkles size="1em" />
|
||||
<span>Fast Input Mode Active</span>
|
||||
</div>
|
||||
@@ -119,26 +136,33 @@
|
||||
|
||||
<!-- Administrative Action Bar (Edit Mode Only) -->
|
||||
{#if $ae_loc.edit_mode}
|
||||
<nav class="flex flex-row flex-wrap gap-3 items-center justify-center w-full py-4 border-y border-surface-500/10">
|
||||
<button type="button"
|
||||
<nav
|
||||
class="flex flex-row flex-wrap gap-3 items-center justify-center w-full py-4 border-y border-surface-500/10"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-secondary shadow-lg hover:scale-105 transition-transform"
|
||||
onclick={() => $journals_sess.show__modal_new__journal_obj = true}
|
||||
onclick={() =>
|
||||
($journals_sess.show__modal_new__journal_obj = true)}
|
||||
>
|
||||
<BookPlus size="1.2em" class="mr-2" />
|
||||
<span>New Journal</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-surface border border-surface-500/30 shadow-lg hover:scale-105 transition-transform"
|
||||
onclick={() => show_import_modal = true}
|
||||
onclick={() => (show_import_modal = true)}
|
||||
>
|
||||
<FileUp size="1.2em" class="mr-2" />
|
||||
<span>Import</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface shadow-lg hover:scale-105 transition-transform"
|
||||
onclick={() => $journals_sess.show__modal__journals_config = true}
|
||||
onclick={() =>
|
||||
($journals_sess.show__modal__journals_config = true)}
|
||||
>
|
||||
<Wrench size="1.2em" class="mr-2" />
|
||||
<span>Config</span>
|
||||
@@ -149,20 +173,29 @@
|
||||
<!-- Main List Section -->
|
||||
<main class="w-full flex justify-center">
|
||||
{#if $lq__journal_obj_li === undefined}
|
||||
<div class="flex flex-col items-center justify-center p-20 gap-4 opacity-50">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-20 gap-4 opacity-50"
|
||||
>
|
||||
<Loader2 size="3em" class="animate-spin" />
|
||||
<p class="text-xl font-bold">Accessing Brain...</p>
|
||||
</div>
|
||||
{:else if $lq__journal_obj_li.length > 0}
|
||||
<Journal_obj_li {lq__journal_obj_li} />
|
||||
{:else}
|
||||
<div class="max-w-md text-center p-12 bg-surface-500/5 rounded-3xl border-2 border-dashed border-surface-500/20">
|
||||
<div
|
||||
class="max-w-md text-center p-12 bg-surface-500/5 rounded-3xl border-2 border-dashed border-surface-500/20"
|
||||
>
|
||||
<SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" />
|
||||
<h3 class="text-2xl font-bold mb-2">No Journals Found</h3>
|
||||
<p class="opacity-60 mb-6">You haven't created any journals yet. Start by creating one to begin your documentation journey.</p>
|
||||
<button type="button"
|
||||
<p class="opacity-60 mb-6">
|
||||
You haven't created any journals yet. Start by creating one
|
||||
to begin your documentation journey.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary"
|
||||
onclick={() => $journals_sess.show__modal_new__journal_obj = true}
|
||||
onclick={() =>
|
||||
($journals_sess.show__modal_new__journal_obj = true)}
|
||||
>
|
||||
Create Your First Journal
|
||||
</button>
|
||||
@@ -182,7 +215,9 @@
|
||||
>
|
||||
<div class="p-2 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<label class="label text-sm font-bold opacity-75">Journal Name</label>
|
||||
<label class="label text-sm font-bold opacity-75"
|
||||
>Journal Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. My Daily Logs"
|
||||
@@ -191,7 +226,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="label text-sm font-bold opacity-75">Type Code</label>
|
||||
<label class="label text-sm font-bold opacity-75"
|
||||
>Type Code</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. diary, log, notebook"
|
||||
@@ -200,19 +237,31 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<button type="button" class="btn variant-soft-surface" onclick={() => $journals_sess.show__modal_new__journal_obj = false}>Cancel</button>
|
||||
<button type="button" class="btn variant-filled-primary font-bold" onclick={create_journal}>Create Journal</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface"
|
||||
onclick={() =>
|
||||
($journals_sess.show__modal_new__journal_obj = false)}
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary font-bold"
|
||||
onclick={create_journal}>Create Journal</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
{#if $journals_sess.show__modal__journals_config}
|
||||
<AeCompModalJournalConfig show={$journals_sess.show__modal__journals_config} />
|
||||
<AeCompModalJournalConfig
|
||||
show={$journals_sess.show__modal__journals_config}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<AeCompModalJournalImport
|
||||
bind:open={show_import_modal}
|
||||
on_close={() => (show_import_modal = false)}
|
||||
on_import_complete={() => {}}
|
||||
/>
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,9 @@ export async function load({ fetch, parent }) {
|
||||
let ae_acct = parent_data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Journals +page.ts: Account ${account_id} not found in parent data. Initializing ghost acct.`);
|
||||
console.warn(
|
||||
`ae Journals +page.ts: Account ${account_id} not found in parent data. Initializing ghost acct.`
|
||||
);
|
||||
ae_acct = {
|
||||
api: parent_data.ae_api || {},
|
||||
loc: {},
|
||||
@@ -30,7 +32,7 @@ export async function load({ fetch, parent }) {
|
||||
const person_id = ae_acct.loc.person_id;
|
||||
|
||||
// OPTIMIZATION: Fire the journal list load in the background.
|
||||
// Components using LiveQuery (db_journals) will display cached data
|
||||
// Components using LiveQuery (db_journals) will display cached data
|
||||
// instantly while this refresh runs.
|
||||
journals_func.load_ae_obj_li__journal({
|
||||
api_cfg: ae_acct.api,
|
||||
|
||||
@@ -35,11 +35,16 @@
|
||||
|
||||
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
|
||||
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)) {
|
||||
if (
|
||||
JSON.stringify($journals_slct.journal_obj) !==
|
||||
JSON.stringify(results)
|
||||
) {
|
||||
$journals_slct.journal_obj = { ...results };
|
||||
}
|
||||
}
|
||||
@@ -50,11 +55,14 @@
|
||||
|
||||
$effect(() => {
|
||||
if (log_lvl) {
|
||||
console.log(`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`);
|
||||
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)
|
||||
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}`,
|
||||
@@ -99,7 +107,8 @@
|
||||
"
|
||||
>
|
||||
<!-- If middle click then open the all journals page in a new tab. Otherwise show/hide the menu. -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onmousedown={(event) => {
|
||||
if (event.button === 1) {
|
||||
// Middle click - open in new tab
|
||||
@@ -107,7 +116,7 @@
|
||||
window.open('/journals');
|
||||
// } else {
|
||||
// // Left click - toggle menu
|
||||
// event.preventDefault(); // Prevent default middle-click behavior
|
||||
// event.prevent_default(); // Prevent default middle-click behavior
|
||||
// show_menu__all_journals = !show_menu__all_journals;
|
||||
}
|
||||
}}
|
||||
@@ -191,13 +200,15 @@ Middle-click to open in new tab`}
|
||||
"
|
||||
>
|
||||
<option value="" disabled selected>
|
||||
{Object.keys($journals_loc.entry_view_history_kv).length}× Recent
|
||||
Entries...
|
||||
{Object.keys($journals_loc.entry_view_history_kv)
|
||||
.length}× Recent Entries...
|
||||
</option>
|
||||
<!-- loop through each key value -->
|
||||
{#each Object.entries($journals_loc.entry_view_history_kv as Record<string, ae_JournalEntry>).reverse() as [journal_entry_id, journal_entry_obj]}
|
||||
{#each Object.entries($journals_loc.entry_view_history_kv as Record).reverse() as [journal_entry_id, journal_entry_obj]}
|
||||
<option value={journal_entry_obj.id}>
|
||||
{(journal_entry_obj?.name || journal_entry_obj?.id) ?? 'NONE'}
|
||||
{(journal_entry_obj?.name ||
|
||||
journal_entry_obj?.id) ??
|
||||
'NONE'}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -237,7 +248,8 @@ Middle-click to open in new tab`}
|
||||
<!-- <span class="text-sm text-gray-500 hidden md:inline">
|
||||
New entry:
|
||||
</span> -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
// $journals_sess.show__modal_new__journal_entry_obj = true;
|
||||
|
||||
@@ -245,10 +257,14 @@ Middle-click to open in new tab`}
|
||||
category_code: null
|
||||
};
|
||||
if ($journals_loc.entry.qry__category_code) {
|
||||
data_kv.category_code = $journals_loc.entry.qry__category_code;
|
||||
data_kv.category_code =
|
||||
$journals_loc.entry.qry__category_code;
|
||||
}
|
||||
if (log_lvl) {
|
||||
console.log('Creating new journal entry with data_kv:', data_kv);
|
||||
console.log(
|
||||
'Creating new journal entry with data_kv:',
|
||||
data_kv
|
||||
);
|
||||
}
|
||||
journals_func
|
||||
.create_ae_obj__journal_entry({
|
||||
@@ -259,16 +275,24 @@ Middle-click to open in new tab`}
|
||||
})
|
||||
.then((results) => {
|
||||
if (log_lvl) {
|
||||
console.log('New journal entry created:', results);
|
||||
console.log(
|
||||
'New journal entry created:',
|
||||
results
|
||||
);
|
||||
}
|
||||
$journals_slct.journal_entry_id = results?.journal_entry_id_random;
|
||||
$journals_slct.journal_entry_id =
|
||||
results?.journal_entry_id_random;
|
||||
// $journals_loc.entry.edit = true;
|
||||
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] =
|
||||
'current';
|
||||
$journals_loc.entry.edit_kv[
|
||||
$journals_slct.journal_entry_id
|
||||
] = 'current';
|
||||
// alert(`Journal entry created successfully! ${$journals_slct.journal_entry_id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error updating journal entry:', error);
|
||||
console.error(
|
||||
'Error updating journal entry:',
|
||||
error
|
||||
);
|
||||
alert('Failed to update journal entry.');
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -299,6 +323,6 @@ Middle-click to open in new tab`}
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto">
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -15,7 +15,9 @@ export async function load({ params, parent }) {
|
||||
let ae_acct = data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Journals [journal_id] +layout.ts: Account ${account_id} not found. Initializing ghost acct.`);
|
||||
console.warn(
|
||||
`ae Journals [journal_id] +layout.ts: Account ${account_id} not found. Initializing ghost acct.`
|
||||
);
|
||||
ae_acct = {
|
||||
api: data.ae_api || {},
|
||||
slct: {
|
||||
@@ -36,10 +38,13 @@ export async function load({ params, parent }) {
|
||||
ae_acct.slct.journal_id = journal_id;
|
||||
|
||||
if (browser) {
|
||||
if (log_lvl) console.log(`ae_journals journals [journal_id] +layout.ts (Non-Blocking)`);
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`ae_journals journals [journal_id] +layout.ts (Non-Blocking)`
|
||||
);
|
||||
|
||||
// OPTIMIZATION: Fire the journal load in the background.
|
||||
// The journal module now uses SWR, and components watching IDB
|
||||
// The journal module now uses SWR, and components watching IDB
|
||||
// will update automatically when the refresh completes.
|
||||
journals_func.load_ae_obj_id__journal({
|
||||
api_cfg: ae_acct.api,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let ae_acct = data[$slct.account_id];
|
||||
let show_export_modal = $state(false);
|
||||
let show_import_modal = $state(false);
|
||||
|
||||
|
||||
let search_id_li: Array<string> = $state([]);
|
||||
let search_debounce_timer: any = null;
|
||||
let last_search_id = 0;
|
||||
@@ -54,7 +54,8 @@
|
||||
|
||||
function handle_import_complete() {
|
||||
// Trigger a refresh of the journal entry list
|
||||
if ($journals_loc.entry.search_version === undefined) $journals_loc.entry.search_version = 0;
|
||||
if ($journals_loc.entry.search_version === undefined)
|
||||
$journals_loc.entry.search_version = 0;
|
||||
$journals_loc.entry.search_version++;
|
||||
}
|
||||
|
||||
@@ -63,12 +64,14 @@
|
||||
|
||||
let lq__journal_obj = $derived(
|
||||
liveQuery(async () => {
|
||||
return await db_journals.journal.get($journals_slct?.journal_id ?? '');
|
||||
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
|
||||
// Re-wrapped in $derived to ensure the observable instance remains stable
|
||||
// unless the underlying dependencies (ids, search context) change.
|
||||
let lq__journal_entry_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
@@ -79,14 +82,18 @@
|
||||
|
||||
// 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`);
|
||||
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);
|
||||
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}`);
|
||||
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)
|
||||
@@ -139,14 +146,17 @@
|
||||
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})...`);
|
||||
|
||||
|
||||
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;
|
||||
|
||||
@@ -160,30 +170,46 @@
|
||||
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;
|
||||
.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);
|
||||
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;
|
||||
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_random).filter(Boolean);
|
||||
local_ids = local_results
|
||||
.map((e) => e.id || e.journal_entry_id_random)
|
||||
.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.`);
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`[Journal Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`
|
||||
);
|
||||
untrack(() => {
|
||||
search_id_li = local_ids;
|
||||
if (local_ids.length > 0) $journals_sess.entry.qry__status = 'done';
|
||||
if (local_ids.length > 0)
|
||||
$journals_sess.entry.qry__status = 'done';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -208,10 +234,17 @@
|
||||
|
||||
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_random).filter(Boolean);
|
||||
|
||||
const api_ids = api_results
|
||||
.map((e: any) => e.id || e.journal_entry_id_random)
|
||||
.filter(Boolean);
|
||||
|
||||
// Protect UI cache if API returns empty during revalidation
|
||||
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) {
|
||||
if (
|
||||
api_ids.length === 0 &&
|
||||
local_ids.length > 0 &&
|
||||
!remote_first &&
|
||||
!qry_str
|
||||
) {
|
||||
untrack(() => {
|
||||
$journals_sess.entry.qry__status = 'done';
|
||||
});
|
||||
@@ -223,7 +256,10 @@
|
||||
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.`);
|
||||
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) {
|
||||
@@ -239,7 +275,10 @@
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
window.parent.postMessage({ journal_id: $journals_slct?.journal_id ?? null }, '*');
|
||||
window.parent.postMessage(
|
||||
{ journal_id: $journals_slct?.journal_id ?? null },
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
@@ -252,7 +291,9 @@
|
||||
</svelte:head>
|
||||
|
||||
{#if $lq__journal_obj === undefined}
|
||||
<div class="flex flex-col items-center justify-center p-20 opacity-50 text-center">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-20 opacity-50 text-center"
|
||||
>
|
||||
<LoaderCircle size="3em" class="animate-spin mb-4 mx-auto" />
|
||||
<p class="text-xl">Loading Journal...</p>
|
||||
</div>
|
||||
@@ -260,15 +301,15 @@
|
||||
<AeCompJournalObjIdView
|
||||
{lq__journal_obj}
|
||||
{lq__journal_entry_obj_li}
|
||||
on_show_export={() => show_export_modal = true}
|
||||
on_show_import={() => show_import_modal = true}
|
||||
on_show_export={() => (show_export_modal = true)}
|
||||
on_show_import={() => (show_import_modal = true)}
|
||||
/>
|
||||
|
||||
<Journal_entry_obj_li_wrapper
|
||||
{lq__journal_obj}
|
||||
<Journal_entry_obj_li_wrapper
|
||||
{lq__journal_obj}
|
||||
{lq__journal_entry_obj_li}
|
||||
show_found_header={false}
|
||||
{log_lvl}
|
||||
{log_lvl}
|
||||
/>
|
||||
|
||||
<Journal_obj_id_edit
|
||||
@@ -286,11 +327,15 @@
|
||||
|
||||
<AeCompModalJournalImport
|
||||
bind:open={show_import_modal}
|
||||
on_close={() => show_import_modal = false}
|
||||
on_close={() => (show_import_modal = false)}
|
||||
on_import_complete={handle_import_complete}
|
||||
/>
|
||||
{:else}
|
||||
<section class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center">
|
||||
<p class="text-center">You must be logged in as the owner to view this Journal.</p>
|
||||
<section
|
||||
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center"
|
||||
>
|
||||
<p class="text-center">
|
||||
You must be logged in as the owner to view this Journal.
|
||||
</p>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -59,11 +59,16 @@
|
||||
$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
|
||||
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)) {
|
||||
if (
|
||||
JSON.stringify($journals_slct.journal_obj) !==
|
||||
JSON.stringify(results)
|
||||
) {
|
||||
$journals_slct.journal_obj = { ...results };
|
||||
}
|
||||
}
|
||||
@@ -74,11 +79,14 @@
|
||||
|
||||
$effect(() => {
|
||||
if (log_lvl) {
|
||||
console.log(`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`);
|
||||
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)
|
||||
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}`,
|
||||
@@ -104,7 +112,8 @@
|
||||
// Check if results are different than the current session version stored under $journals_slct
|
||||
if (
|
||||
$journals_slct.journal_obj_li &&
|
||||
JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(results)
|
||||
JSON.stringify($journals_slct.journal_obj_li) !==
|
||||
JSON.stringify(results)
|
||||
) {
|
||||
$journals_slct.journal_obj_li = [...results];
|
||||
}
|
||||
@@ -119,7 +128,8 @@
|
||||
console.log(`lq__journal_obj_li: results = `, lq__journal_obj_li);
|
||||
if (
|
||||
$journals_slct.journal_obj_li &&
|
||||
JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(lq__journal_obj_li)
|
||||
JSON.stringify($journals_slct.journal_obj_li) !==
|
||||
JSON.stringify(lq__journal_obj_li)
|
||||
) {
|
||||
console.log(
|
||||
`Session slct li stored version has changed for ID = ${$ae_loc.person_id}`,
|
||||
@@ -147,7 +157,10 @@
|
||||
|
||||
// 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)) {
|
||||
if (
|
||||
JSON.stringify($journals_slct.journal_entry_obj) !==
|
||||
JSON.stringify(results)
|
||||
) {
|
||||
$journals_slct.journal_entry_obj = { ...results };
|
||||
}
|
||||
}
|
||||
@@ -161,7 +174,10 @@
|
||||
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);
|
||||
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) !==
|
||||
@@ -180,11 +196,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$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 ?? {}) };
|
||||
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';
|
||||
@@ -220,7 +237,8 @@
|
||||
|
||||
// Only update if changed
|
||||
if (
|
||||
JSON.stringify(history_kv) !== JSON.stringify($journals_loc?.entry_view_history_kv)
|
||||
JSON.stringify(history_kv) !==
|
||||
JSON.stringify($journals_loc?.entry_view_history_kv)
|
||||
) {
|
||||
$journals_loc.entry_view_history_kv = history_kv;
|
||||
console.log(
|
||||
@@ -229,7 +247,9 @@
|
||||
);
|
||||
} else {
|
||||
if (log_lvl > 1) {
|
||||
console.log(`$journals_loc.entry_view_history_kv has not changed.`);
|
||||
console.log(
|
||||
`$journals_loc.entry_view_history_kv has not changed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +281,12 @@
|
||||
"
|
||||
>
|
||||
<!-- {#if $lq__journal_entry_obj} -->
|
||||
<Journal_entry_view {lq__journal_obj} {lq__journal_obj_li} {lq__journal_entry_obj} on_show_export={() => show_export_modal = true} />
|
||||
<Journal_entry_view
|
||||
{lq__journal_obj}
|
||||
{lq__journal_obj_li}
|
||||
{lq__journal_entry_obj}
|
||||
on_show_export={() => (show_export_modal = true)}
|
||||
/>
|
||||
<!-- {/if} -->
|
||||
</section>
|
||||
|
||||
@@ -269,10 +294,14 @@
|
||||
bind:open={show_export_modal}
|
||||
entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []}
|
||||
journal={$lq__journal_obj}
|
||||
on_close={() => show_export_modal = false}
|
||||
on_close={() => (show_export_modal = false)}
|
||||
/>
|
||||
{:else}
|
||||
<section class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center">
|
||||
<p class="text-center">You must be logged in as the owner to view this Journal Entry.</p>
|
||||
<section
|
||||
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center"
|
||||
>
|
||||
<p class="text-center">
|
||||
You must be logged in as the owner to view this Journal Entry.
|
||||
</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,10 @@ console.log(`ae_p_journals [journal_entry_id] +page.ts start`);
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
import { db_journals, journal_entry_field_li } from '$lib/ae_journals/db_journals';
|
||||
import {
|
||||
db_journals,
|
||||
journal_entry_field_li
|
||||
} from '$lib/ae_journals/db_journals';
|
||||
import { load_ae_obj_id } from '$lib/ae_core/core__crud_generic';
|
||||
|
||||
export async function load({ params, parent }) {
|
||||
@@ -18,7 +21,9 @@ export async function load({ params, parent }) {
|
||||
let ae_acct = data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Journals [journal_entry_id] +page.ts: Account ${account_id} not found. Initializing ghost acct.`);
|
||||
console.warn(
|
||||
`ae Journals [journal_entry_id] +page.ts: Account ${account_id} not found. Initializing ghost acct.`
|
||||
);
|
||||
ae_acct = {
|
||||
api: data.ae_api || {},
|
||||
slct: {
|
||||
@@ -72,7 +77,9 @@ export async function load({ params, parent }) {
|
||||
});
|
||||
|
||||
if (!load_journal_entry_obj) {
|
||||
console.warn(`ae Journals [journal_entry_id] +page.ts: Entry ${journal_entry_id} not found via API or Cache.`);
|
||||
console.warn(
|
||||
`ae Journals [journal_entry_id] +page.ts: Entry ${journal_entry_id} not found via API or Cache.`
|
||||
);
|
||||
// error(404, {
|
||||
// message: 'Journals - Journal Entry not found'
|
||||
// });
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
content,
|
||||
summary = $bindable(),
|
||||
on_save,
|
||||
log_lvl = 0
|
||||
let {
|
||||
content,
|
||||
summary = $bindable(),
|
||||
on_save,
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="journal-entry-ai-tools absolute top-2 right-2 z-10">
|
||||
<AE_AITools
|
||||
{content}
|
||||
bind:summary={summary}
|
||||
bind:summary
|
||||
onSave={(newSummary) => {
|
||||
summary = newSummary;
|
||||
on_save();
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
import { LockKeyhole, Save, RefreshCcw } from 'lucide-svelte';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import { journals_loc, journals_sess } from '$lib/ae_journals/ae_journals_stores';
|
||||
import {
|
||||
journals_loc,
|
||||
journals_sess
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/AE_Comp_Editor_CodeMirror.svelte';
|
||||
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
|
||||
|
||||
@@ -21,10 +24,10 @@
|
||||
on_force_reset?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
entry,
|
||||
journal,
|
||||
tmp_entry_obj = $bindable(),
|
||||
let {
|
||||
entry,
|
||||
journal,
|
||||
tmp_entry_obj = $bindable(),
|
||||
editor_view = $bindable(),
|
||||
has_changed,
|
||||
updated_idb,
|
||||
@@ -32,10 +35,14 @@
|
||||
on_force_reset
|
||||
}: Props = $props();
|
||||
|
||||
const is_editing = $derived($journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current');
|
||||
const is_editing = $derived(
|
||||
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="journal-entry-editor-wrapper grow w-full flex flex-col items-center">
|
||||
<div
|
||||
class="journal-entry-editor-wrapper grow w-full flex flex-col items-center"
|
||||
>
|
||||
{#if !is_editing}
|
||||
<!-- VIEW MODE -->
|
||||
<div class="w-full max-w-6xl p-4 prose dark:prose-invert">
|
||||
@@ -45,27 +52,40 @@
|
||||
<!-- EDIT MODE -->
|
||||
{#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted}
|
||||
<!-- Decryption Required Message -->
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="font-bold flex items-center gap-2">
|
||||
<LockKeyhole size="1.25em" />
|
||||
Decryption Required
|
||||
</div>
|
||||
<p class="text-sm">This entry must be decrypted before it can be edited.</p>
|
||||
<p class="text-sm">
|
||||
This entry must be decrypted before it can be edited.
|
||||
</p>
|
||||
{#if tmp_entry_obj?.content === false}
|
||||
<p class="text-xs font-bold text-error-500 uppercase tracking-widest">Decryption failed. Incorrect passcode.</p>
|
||||
<p
|
||||
class="text-xs font-bold text-error-500 uppercase tracking-widest"
|
||||
>
|
||||
Decryption failed. Incorrect passcode.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $ae_loc.edit_mode && on_force_reset}
|
||||
<div class="pt-4 border-t border-error-500/20">
|
||||
<p class="text-xs mb-2 opacity-70 italic">Passcode lost? You can force a reset to plain text, but all currently encrypted data will be permanently deleted.</p>
|
||||
<button type="button"
|
||||
|
||||
<p class="text-xs mb-2 opacity-70 italic">
|
||||
Passcode lost? You can force a reset to plain text,
|
||||
but all currently encrypted data will be permanently
|
||||
deleted.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-error font-bold"
|
||||
onclick={on_force_reset}
|
||||
>
|
||||
<RefreshCcw size="1.1em" class="mr-2" /> Force Reset to Plain Text
|
||||
<RefreshCcw size="1.1em" class="mr-2" /> Force Reset to
|
||||
Plain Text
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -75,7 +95,7 @@
|
||||
{#if journal?.cfg_json?.pref_editor == 'codemirror'}
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
bind:content={tmp_entry_obj.content}
|
||||
bind:editor_view={editor_view}
|
||||
bind:editor_view
|
||||
theme_mode={$ae_loc.theme_mode}
|
||||
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"
|
||||
@@ -89,7 +109,8 @@
|
||||
{/if}
|
||||
|
||||
<!-- Floating Save Button -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={on_save}
|
||||
disabled={!has_changed}
|
||||
class="btn btn-sm md:btn-md lg:btn-lg fixed top-72 right-6 min-w-32 variant-filled-success shadow-xl z-20 transition-all"
|
||||
@@ -99,7 +120,8 @@
|
||||
</button>
|
||||
|
||||
<!-- Inline Save Button (Mobile/Context) -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={on_save}
|
||||
disabled={!has_changed}
|
||||
class="btn variant-filled-warning w-full max-w-96 mt-4"
|
||||
@@ -109,7 +131,9 @@
|
||||
</button>
|
||||
|
||||
{#if updated_idb}
|
||||
<p class="text-xs text-error-500 mt-2 font-bold animate-pulse uppercase tracking-widest">
|
||||
<p
|
||||
class="text-xs text-error-500 mt-2 font-bold animate-pulse uppercase tracking-widest"
|
||||
>
|
||||
IDB object updated since last load!
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -4,13 +4,25 @@
|
||||
* Standardized Journal Entry Header.
|
||||
* Manages name, sync status, and triggers the modular config.
|
||||
*/
|
||||
import {
|
||||
Save, Eye, Pencil,
|
||||
Fingerprint, LockKeyhole, LockKeyholeOpen, Settings,
|
||||
ChevronLeft, CircleCheck, CircleX, Loader2, RefreshCw
|
||||
import {
|
||||
Save,
|
||||
Eye,
|
||||
Pencil,
|
||||
Fingerprint,
|
||||
LockKeyhole,
|
||||
LockKeyholeOpen,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Loader2,
|
||||
RefreshCw
|
||||
} 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 {
|
||||
journals_loc,
|
||||
journals_sess
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
|
||||
|
||||
interface Props {
|
||||
@@ -26,22 +38,26 @@
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
entry,
|
||||
journal,
|
||||
tmp_entry_obj = $bindable(),
|
||||
let {
|
||||
entry,
|
||||
journal,
|
||||
tmp_entry_obj = $bindable(),
|
||||
has_changed,
|
||||
save_status = 'saved',
|
||||
on_save,
|
||||
on_decrypt,
|
||||
on_show_config,
|
||||
log_lvl = 0
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
|
||||
const is_decrypted = $derived($journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted === true);
|
||||
const is_decrypted = $derived(
|
||||
$journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted ===
|
||||
true
|
||||
);
|
||||
|
||||
function toggle_edit_mode() {
|
||||
const isEditing = $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current';
|
||||
const isEditing =
|
||||
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current';
|
||||
if (isEditing) {
|
||||
if (has_changed) on_save();
|
||||
else $journals_loc.entry.edit_kv[entry.journal_entry_id] = false;
|
||||
@@ -51,19 +67,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="flex flex-col md:flex-row items-center justify-between gap-4 p-3 bg-surface-100/50 dark:bg-surface-900/50 rounded-xl border border-surface-500/20 backdrop-blur-md shadow-sm w-full">
|
||||
<header
|
||||
class="flex flex-col md:flex-row items-center justify-between gap-4 p-3 bg-surface-100/50 dark:bg-surface-900/50 rounded-xl border border-surface-500/20 backdrop-blur-md shadow-sm w-full"
|
||||
>
|
||||
<div class="flex items-center gap-3 w-full md:w-auto">
|
||||
<a href="/journals/{journal.journal_id}" class="btn-icon btn-icon-sm variant-soft-surface" title="Back to Journal">
|
||||
<a
|
||||
href="/journals/{journal.journal_id}"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
title="Back to Journal"
|
||||
>
|
||||
<ChevronLeft size="1.2em" />
|
||||
</a>
|
||||
|
||||
|
||||
<div class="flex items-center gap-2 grow">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggle_edit_mode}
|
||||
class="btn-icon btn-icon-sm transition-all {has_changed && $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current' ? 'variant-filled-success' : 'variant-soft-surface'}"
|
||||
class="btn-icon btn-icon-sm transition-all {has_changed &&
|
||||
$journals_loc.entry.edit_kv[entry.journal_entry_id] ===
|
||||
'current'
|
||||
? 'variant-filled-success'
|
||||
: 'variant-soft-surface'}"
|
||||
>
|
||||
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
|
||||
{#if has_changed}<Save size="1.2em" />{:else}<Eye size="1.2em" />{/if}
|
||||
{#if has_changed}<Save size="1.2em" />{:else}<Eye
|
||||
size="1.2em"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<Pencil size="1.2em" />
|
||||
{/if}
|
||||
@@ -79,7 +108,11 @@
|
||||
/>
|
||||
{:else}
|
||||
<h2 class="text-base md:text-lg font-bold truncate max-w-md">
|
||||
{entry.name || ae_util.iso_datetime_formatter(entry.created_on, 'datetime_iso_12_no_seconds')}
|
||||
{entry.name ||
|
||||
ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'datetime_iso_12_no_seconds'
|
||||
)}
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -88,17 +121,30 @@
|
||||
<div class="flex items-center gap-2 w-full md:w-auto justify-end">
|
||||
<!-- Auto-Save indicator -->
|
||||
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
|
||||
<div class="flex items-center gap-1 px-2 border-r border-surface-500/20 mr-1">
|
||||
<button type="button"
|
||||
class="btn-icon btn-icon-sm { $journals_loc.entry.auto_save ? 'text-primary-500' : 'opacity-30'}"
|
||||
onclick={() => $journals_loc.entry.auto_save = !$journals_loc.entry.auto_save}
|
||||
<div
|
||||
class="flex items-center gap-1 px-2 border-r border-surface-500/20 mr-1"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm {$journals_loc.entry.auto_save
|
||||
? 'text-primary-500'
|
||||
: 'opacity-30'}"
|
||||
onclick={() =>
|
||||
($journals_loc.entry.auto_save =
|
||||
!$journals_loc.entry.auto_save)}
|
||||
title="Toggle Auto Save"
|
||||
>
|
||||
<RefreshCw size="1em" />
|
||||
</button>
|
||||
{#if $journals_loc.entry.auto_save}
|
||||
{#if save_status === 'saving'}<Loader2 size="1em" class="animate-spin text-primary-500" />
|
||||
{:else if save_status === 'saved'}<CircleCheck size="1em" class="text-success-500" />
|
||||
{#if save_status === 'saving'}<Loader2
|
||||
size="1em"
|
||||
class="animate-spin text-primary-500"
|
||||
/>
|
||||
{:else if save_status === 'saved'}<CircleCheck
|
||||
size="1em"
|
||||
class="text-success-500"
|
||||
/>
|
||||
{:else}<CircleX size="1em" class="opacity-30" />{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -106,20 +152,25 @@
|
||||
|
||||
<!-- Decrypt Toggle (Lock) -->
|
||||
{#if entry.private}
|
||||
<button type="button"
|
||||
class="btn-icon btn-icon-sm transition-all {is_decrypted ? 'variant-filled-success shadow-lg shadow-success-500/20' : 'variant-soft-warning'}"
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm transition-all {is_decrypted
|
||||
? 'variant-filled-success shadow-lg shadow-success-500/20'
|
||||
: 'variant-soft-warning'}"
|
||||
onclick={on_decrypt}
|
||||
title={is_decrypted ? 'Lock Content' : 'Decrypt Content'}
|
||||
>
|
||||
{#if is_decrypted}<LockKeyholeOpen size="1.2em" />{:else}<LockKeyhole size="1.2em" />{/if}
|
||||
{#if is_decrypted}<LockKeyholeOpen
|
||||
size="1.2em"
|
||||
/>{:else}<LockKeyhole size="1.2em" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="w-[1px] h-6 bg-surface-500/20 mx-1"></div>
|
||||
|
||||
<!-- Unified Config Button -->
|
||||
<button type="button"
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-primary font-bold"
|
||||
onclick={on_show_config}
|
||||
>
|
||||
@@ -128,9 +179,13 @@
|
||||
|
||||
<!-- Explicit Save (Mobile/Backup) -->
|
||||
{#if has_changed && save_status !== 'saving'}
|
||||
<button type="button" class="btn btn-sm variant-filled-primary shadow-lg" onclick={on_save}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-primary shadow-lg"
|
||||
onclick={on_save}
|
||||
>
|
||||
<Save size="1.1em" class="mr-2" /> Save
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
@@ -17,16 +17,24 @@
|
||||
|
||||
<section class="journal-metadata w-full space-y-4">
|
||||
<!-- System Timestamps -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1 text-xs text-surface-500">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1 text-xs text-surface-500"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<span title="Creation date">
|
||||
<span class="font-semibold">Created:</span>
|
||||
{ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long')}
|
||||
<span class="font-semibold">Created:</span>
|
||||
{ae_util.iso_datetime_formatter(
|
||||
entry.created_on,
|
||||
'datetime_12_long'
|
||||
)}
|
||||
</span>
|
||||
{#if entry.updated_on}
|
||||
<span title="Last update">
|
||||
<span class="font-semibold">Updated:</span>
|
||||
{ae_util.iso_datetime_formatter(entry.updated_on, 'datetime_12_long')}
|
||||
<span class="font-semibold">Updated:</span>
|
||||
{ae_util.iso_datetime_formatter(
|
||||
entry.updated_on,
|
||||
'datetime_12_long'
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,13 @@
|
||||
|
||||
// *** Import Lucide Icons
|
||||
import {
|
||||
FileUp, Trash2, Download, Paperclip,
|
||||
ExternalLink, Loader2, RefreshCw
|
||||
FileUp,
|
||||
Trash2,
|
||||
Download,
|
||||
Paperclip,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
RefreshCw
|
||||
} from 'lucide-svelte';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
@@ -45,7 +50,12 @@
|
||||
lq__journal_entry_obj: any;
|
||||
}
|
||||
|
||||
let { log_lvl = 0, link_to_type, link_to_id, lq__journal_entry_obj }: Props = $props();
|
||||
let {
|
||||
log_lvl = 0,
|
||||
link_to_type,
|
||||
link_to_id,
|
||||
lq__journal_entry_obj
|
||||
}: Props = $props();
|
||||
|
||||
// *** State
|
||||
let ae_promises: Record<string, any> = $state({});
|
||||
@@ -74,11 +84,20 @@
|
||||
|
||||
// 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 });
|
||||
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;
|
||||
@@ -91,7 +110,10 @@
|
||||
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])
|
||||
updated_files.map((f) => [
|
||||
f.hosted_file_id || f.id || f.hosted_file_id,
|
||||
f
|
||||
])
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -112,8 +134,15 @@
|
||||
|
||||
// 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 };
|
||||
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;
|
||||
@@ -125,8 +154,12 @@
|
||||
|
||||
// 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 => ({
|
||||
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
|
||||
}));
|
||||
@@ -140,9 +173,12 @@
|
||||
});
|
||||
|
||||
async function handle_remove_file(file_id: string) {
|
||||
if (!confirm('Are you sure you want to remove this file attachment?')) return;
|
||||
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);
|
||||
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) {
|
||||
@@ -163,24 +199,34 @@
|
||||
|
||||
<section class="ae_section journal_entry_files w-full space-y-4 my-2">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-surface-500/20 pb-2">
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-surface-500/20 pb-2"
|
||||
>
|
||||
<h3 class="h3 flex items-center gap-2 text-lg font-bold">
|
||||
<Paperclip size="1.1em" />
|
||||
Attachments
|
||||
{#if unified_file_li.length}
|
||||
<span class="badge variant-soft-surface text-xs">{unified_file_li.length}</span>
|
||||
<span class="badge variant-soft-surface text-xs"
|
||||
>{unified_file_li.length}</span
|
||||
>
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-warning font-semibold"
|
||||
onclick={() => {
|
||||
$ae_sess.files.add_to_use_files_method = ($ae_sess.files.add_to_use_files_method === 'upload') ? 'select' : 'upload';
|
||||
$ae_sess.files.add_to_use_files_method =
|
||||
$ae_sess.files.add_to_use_files_method === 'upload'
|
||||
? 'select'
|
||||
: 'upload';
|
||||
}}
|
||||
>
|
||||
<RefreshCw size="1em" class="mr-2" />
|
||||
{$ae_sess.files.add_to_use_files_method === 'select' ? 'Switch to Upload' : 'Switch to Select'}
|
||||
{$ae_sess.files.add_to_use_files_method === 'select'
|
||||
? 'Switch to Upload'
|
||||
: 'Switch to Select'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -189,8 +235,11 @@
|
||||
{#if unified_file_li.length}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{#each unified_file_li as file}
|
||||
{@const file_id = file.hosted_file_id || file.id || file.hosted_file_id}
|
||||
<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">
|
||||
{@const file_id =
|
||||
file.hosted_file_id || file.id || file.hosted_file_id}
|
||||
<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"
|
||||
>
|
||||
<div class="flex items-center gap-3 overflow-hidden grow">
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file_id}
|
||||
@@ -200,13 +249,15 @@
|
||||
classes="w-full !justify-start !px-2"
|
||||
show_divider={true}
|
||||
max_filename={25}
|
||||
show_direct_download={$ae_loc.trusted_access && $ae_loc.edit_mode}
|
||||
show_direct_download={$ae_loc.trusted_access &&
|
||||
$ae_loc.edit_mode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 ml-2">
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-error"
|
||||
onclick={() => handle_remove_file(file_id)}
|
||||
title="Remove attachment"
|
||||
@@ -219,14 +270,18 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !$ae_loc.edit_mode}
|
||||
<p class="text-sm text-surface-500 italic p-4 text-center bg-surface-500/5 rounded-xl">
|
||||
<p
|
||||
class="text-sm text-surface-500 italic p-4 text-center bg-surface-500/5 rounded-xl"
|
||||
>
|
||||
No files attached to this entry.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Edit/Management Tools -->
|
||||
{#if $ae_loc.edit_mode}
|
||||
<div class="mt-4 p-4 rounded-2xl bg-surface-500/5 border-2 border-dashed border-surface-500/20">
|
||||
<div
|
||||
class="mt-4 p-4 rounded-2xl bg-surface-500/5 border-2 border-dashed border-surface-500/20"
|
||||
>
|
||||
{#if $ae_sess.files.add_to_use_files_method === 'upload'}
|
||||
<Comp_hosted_files_upload
|
||||
{link_to_type}
|
||||
@@ -240,19 +295,25 @@
|
||||
>
|
||||
{#snippet label()}
|
||||
<div class="flex flex-col items-center gap-2 py-2">
|
||||
<div class="p-3 rounded-full bg-primary-500/10 text-primary-500">
|
||||
<div
|
||||
class="p-3 rounded-full bg-primary-500/10 text-primary-500"
|
||||
>
|
||||
<FileUp size="1.8em" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="font-bold">Upload new attachment</p>
|
||||
<p class="text-xs opacity-60">Drag and drop or click to browse</p>
|
||||
<p class="text-xs opacity-60">
|
||||
Drag and drop or click to browse
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Comp_hosted_files_upload>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-bold opacity-70 ml-1">Select from existing hosted files:</p>
|
||||
<p class="text-sm font-bold opacity-70 ml-1">
|
||||
Select from existing hosted files:
|
||||
</p>
|
||||
<Element_manage_hosted_file_li_wrap
|
||||
link_to_type={'account'}
|
||||
link_to_id={$ae_loc?.account_id}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Corrected for V3 API Strictness & Robust Decryption
|
||||
* Uses strictly snake_case.
|
||||
*/
|
||||
|
||||
|
||||
// *** Import Svelte core
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
@@ -17,10 +17,14 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import { journals_loc, journals_sess, journals_slct } from '$lib/ae_journals/ae_journals_stores';
|
||||
import {
|
||||
journals_loc,
|
||||
journals_sess,
|
||||
journals_slct
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
import { decrypt_journal_entry } from '$lib/ae_journals/ae_journals_decryption';
|
||||
|
||||
|
||||
import AeCompJournalEntryEditor from './ae_comp__journal_entry_editor.svelte';
|
||||
import AeCompJournalEntryHeader from './ae_comp__journal_entry_header.svelte';
|
||||
import AeCompJournalEntryMetadata from './ae_comp__journal_entry_metadata.svelte';
|
||||
@@ -28,7 +32,7 @@
|
||||
import AeCompJournalEntryObjFileLi from './ae_comp__journal_entry_obj_file_li.svelte';
|
||||
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
|
||||
import AeCompModalJournalEntryConfig from './ae_comp__modal_journal_entry_config.svelte';
|
||||
|
||||
|
||||
// Icons
|
||||
import { AlertCircle, XCircle, Loader2 } from 'lucide-svelte';
|
||||
|
||||
@@ -86,20 +90,38 @@
|
||||
// *** Derived
|
||||
let has_unsaved_changes = $derived.by(() => {
|
||||
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
|
||||
|
||||
const normalize = (val: any) => (val === null || val === undefined) ? '' : String(val).trim();
|
||||
|
||||
const content_changed = normalize(tmp_entry_obj.content) !== normalize(orig_entry_obj.content);
|
||||
const name_changed = normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name);
|
||||
const private_changed = (tmp_entry_obj.private ?? false) !== (orig_entry_obj.private ?? false);
|
||||
|
||||
|
||||
const normalize = (val: any) =>
|
||||
val === null || val === undefined ? '' : String(val).trim();
|
||||
|
||||
const content_changed =
|
||||
normalize(tmp_entry_obj.content) !==
|
||||
normalize(orig_entry_obj.content);
|
||||
const name_changed =
|
||||
normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name);
|
||||
const private_changed =
|
||||
(tmp_entry_obj.private ?? false) !==
|
||||
(orig_entry_obj.private ?? false);
|
||||
|
||||
if (content_changed || name_changed || private_changed) return true;
|
||||
|
||||
const other_fields = ['tags', 'category_code', 'public', 'personal', 'professional', 'archive_on', 'sort'];
|
||||
const other_fields = [
|
||||
'tags',
|
||||
'category_code',
|
||||
'public',
|
||||
'personal',
|
||||
'professional',
|
||||
'archive_on',
|
||||
'sort'
|
||||
];
|
||||
for (const field of other_fields) {
|
||||
if (normalize(tmp_entry_obj[field]) !== normalize(orig_entry_obj[field])) return true;
|
||||
if (
|
||||
normalize(tmp_entry_obj[field]) !==
|
||||
normalize(orig_entry_obj[field])
|
||||
)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -108,16 +130,21 @@
|
||||
// 1. Initial Load & Background Sync
|
||||
$effect(() => {
|
||||
const entry = $lq__journal_entry_obj; // Track only entry
|
||||
|
||||
|
||||
untrack(() => {
|
||||
const journal = $lq__journal_obj;
|
||||
if (!entry || !(entry.updated_on || entry.created_on)) return;
|
||||
|
||||
const session_kv = $journals_sess?.journal_kv[journal?.id];
|
||||
const is_decrypted = session_kv?.journal_passcode_decrypted === true;
|
||||
const is_decrypted =
|
||||
session_kv?.journal_passcode_decrypted === true;
|
||||
|
||||
// Only sync if saved and not currently processing/editing
|
||||
if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
|
||||
if (
|
||||
save_status === 'saved' &&
|
||||
!has_unsaved_changes &&
|
||||
!is_processing
|
||||
) {
|
||||
// Prevent overwrite of recovered text if we are in a decrypted session
|
||||
if (is_decrypted && tmp_entry_obj.content && !entry.content) {
|
||||
return;
|
||||
@@ -141,16 +168,30 @@
|
||||
const _private = tmp_entry_obj.private;
|
||||
|
||||
// Isolate logic from secondary dependencies
|
||||
const should_save = untrack(() => has_unsaved_changes && !is_processing && save_status !== 'saving');
|
||||
const should_save = untrack(
|
||||
() =>
|
||||
has_unsaved_changes &&
|
||||
!is_processing &&
|
||||
save_status !== 'saving'
|
||||
);
|
||||
|
||||
if (should_save) {
|
||||
if (save_status !== 'saving') save_status = 'unsaved';
|
||||
|
||||
const auto_save_enabled = untrack(() => $journals_loc.entry.auto_save);
|
||||
|
||||
const auto_save_enabled = untrack(
|
||||
() => $journals_loc.entry.auto_save
|
||||
);
|
||||
if (auto_save_enabled) {
|
||||
clearTimeout(auto_save_timer);
|
||||
auto_save_timer = setTimeout(() => {
|
||||
if (untrack(() => has_unsaved_changes && !is_processing && save_status !== 'saving')) {
|
||||
if (
|
||||
untrack(
|
||||
() =>
|
||||
has_unsaved_changes &&
|
||||
!is_processing &&
|
||||
save_status !== 'saving'
|
||||
)
|
||||
) {
|
||||
update_journal_entry();
|
||||
}
|
||||
}, 2000);
|
||||
@@ -172,7 +213,12 @@
|
||||
const decrypted_status = session?.journal_passcode_decrypted;
|
||||
const has_encrypted_content = !!entry.content_encrypted;
|
||||
|
||||
if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
|
||||
if (
|
||||
has_encrypted_content &&
|
||||
is_verified &&
|
||||
decrypted_status !== true &&
|
||||
decrypted_status !== 'processing'
|
||||
) {
|
||||
untrack(() => run_decryption_workflow());
|
||||
}
|
||||
});
|
||||
@@ -183,22 +229,27 @@
|
||||
const journal = $lq__journal_obj;
|
||||
if (!journal?.id || is_processing) return;
|
||||
|
||||
let journal_key = $journals_sess.journal_kv[journal.id]?.typed_journal_passcode;
|
||||
|
||||
let journal_key =
|
||||
$journals_sess.journal_kv[journal.id]?.typed_journal_passcode;
|
||||
|
||||
is_processing = true;
|
||||
decryption_error = null;
|
||||
|
||||
journals_sess.update(s => {
|
||||
|
||||
journals_sess.update((s) => {
|
||||
if (!s.journal_kv[journal.id]) s.journal_kv[journal.id] = {};
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = 'processing';
|
||||
return s;
|
||||
});
|
||||
|
||||
const result = await decrypt_journal_entry($lq__journal_entry_obj, journal, journal_key);
|
||||
const result = await decrypt_journal_entry(
|
||||
$lq__journal_entry_obj,
|
||||
journal,
|
||||
journal_key
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
decryption_error = result.error || 'Decryption failed.';
|
||||
journals_sess.update(s => {
|
||||
journals_sess.update((s) => {
|
||||
s.journal_kv[journal.id].journal_passcode_verified = false;
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = false;
|
||||
return s;
|
||||
@@ -225,7 +276,7 @@
|
||||
if (orig_entry_obj) orig_entry_obj.history = result.history;
|
||||
}
|
||||
|
||||
journals_sess.update(s => {
|
||||
journals_sess.update((s) => {
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = true;
|
||||
return s;
|
||||
});
|
||||
@@ -233,8 +284,13 @@
|
||||
}
|
||||
|
||||
async function update_journal_entry(fields_kv?: key_val) {
|
||||
if (!$ae_loc.trusted_access || save_status === 'saving' || is_processing) return;
|
||||
|
||||
if (
|
||||
!$ae_loc.trusted_access ||
|
||||
save_status === 'saving' ||
|
||||
is_processing
|
||||
)
|
||||
return;
|
||||
|
||||
is_processing = true;
|
||||
save_status = 'saving';
|
||||
|
||||
@@ -259,7 +315,9 @@
|
||||
group: tmp_entry_obj.group,
|
||||
data_json: tmp_entry_obj.data_json,
|
||||
archive_on: tmp_entry_obj.archive_on,
|
||||
linked_li_json: tmp_entry_obj.linked_li_json ? JSON.stringify(tmp_entry_obj.linked_li_json) : null
|
||||
linked_li_json: tmp_entry_obj.linked_li_json
|
||||
? JSON.stringify(tmp_entry_obj.linked_li_json)
|
||||
: null
|
||||
};
|
||||
|
||||
const decrypt_key = $lq__journal_obj.combined_passcode;
|
||||
@@ -268,7 +326,10 @@
|
||||
if (!fields_kv) {
|
||||
if (tmp_entry_obj.private) {
|
||||
if (tmp_entry_obj.content) {
|
||||
data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key);
|
||||
data_kv.content_encrypted = await ae_util.encrypt_wrapper(
|
||||
tmp_entry_obj.content,
|
||||
decrypt_key
|
||||
);
|
||||
data_kv.content = null;
|
||||
}
|
||||
} else {
|
||||
@@ -284,7 +345,7 @@
|
||||
data_kv: data_kv,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
|
||||
// Sync ORIG after save to clear unsaved changes flag
|
||||
if (!fields_kv) {
|
||||
orig_entry_obj = deep_copy(tmp_entry_obj);
|
||||
@@ -313,7 +374,7 @@
|
||||
orig_entry_obj.content = null;
|
||||
orig_entry_obj.history = null;
|
||||
}
|
||||
journals_sess.update(s => {
|
||||
journals_sess.update((s) => {
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = false;
|
||||
return s;
|
||||
});
|
||||
@@ -327,23 +388,27 @@
|
||||
}
|
||||
|
||||
async function handle_force_reset() {
|
||||
if (!confirm('WARNING: This will permanently DELETE the encrypted content and history for this entry and reset it to plain text. This cannot be undone. Proceed?')) {
|
||||
if (
|
||||
!confirm(
|
||||
'WARNING: This will permanently DELETE the encrypted content and history for this entry and reset it to plain text. This cannot be undone. Proceed?'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
is_processing = true;
|
||||
tmp_entry_obj.private = false;
|
||||
tmp_entry_obj.content = "";
|
||||
tmp_entry_obj.content = '';
|
||||
tmp_entry_obj.content_encrypted = null;
|
||||
tmp_entry_obj.history = "";
|
||||
tmp_entry_obj.history = '';
|
||||
tmp_entry_obj.history_encrypted = null;
|
||||
|
||||
|
||||
// Sync orig to match the "cleared" state
|
||||
if (orig_entry_obj) {
|
||||
orig_entry_obj.private = false;
|
||||
orig_entry_obj.content = "";
|
||||
orig_entry_obj.content = '';
|
||||
orig_entry_obj.content_encrypted = null;
|
||||
orig_entry_obj.history = "";
|
||||
orig_entry_obj.history = '';
|
||||
orig_entry_obj.history_encrypted = null;
|
||||
}
|
||||
|
||||
@@ -355,7 +420,10 @@
|
||||
let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
|
||||
</script>
|
||||
|
||||
<section class="ae_view flex flex-col gap-2 w-full h-full p-1" bind:clientHeight={$ae_loc.iframe_height_modal_body}>
|
||||
<section
|
||||
class="ae_view flex flex-col gap-2 w-full h-full p-1"
|
||||
bind:clientHeight={$ae_loc.iframe_height_modal_body}
|
||||
>
|
||||
{#if $lq__journal_entry_obj && $lq__journal_obj}
|
||||
<AeCompJournalEntryHeader
|
||||
entry={$lq__journal_entry_obj}
|
||||
@@ -365,26 +433,37 @@
|
||||
has_changed={has_unsaved_changes}
|
||||
on_save={() => update_journal_entry()}
|
||||
on_decrypt={handle_content_decryption}
|
||||
on_show_config={() => show_config_modal = true}
|
||||
on_show_config={() => (show_config_modal = true)}
|
||||
{save_status}
|
||||
{log_lvl}
|
||||
/>
|
||||
|
||||
|
||||
{#if decryption_error}
|
||||
<div class="w-full p-4 bg-error-500/20 border-2 border-error-500 rounded-lg flex items-center justify-between shadow-2xl z-50 animate-bounce">
|
||||
<div class="flex items-center gap-4 text-error-700 dark:text-error-300 font-bold">
|
||||
<div
|
||||
class="w-full p-4 bg-error-500/20 border-2 border-error-500 rounded-lg flex items-center justify-between shadow-2xl z-50 animate-bounce"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-4 text-error-700 dark:text-error-300 font-bold"
|
||||
>
|
||||
<AlertCircle size="2.5em" />
|
||||
<span class="text-xl">{decryption_error}</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm variant-filled-error shadow-lg font-bold" onclick={() => decryption_error = null}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-error shadow-lg font-bold"
|
||||
onclick={() => (decryption_error = null)}
|
||||
>
|
||||
<XCircle size="1.2em" class="mr-2" /> Dismiss
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section class="grow relative bg-gray-100 dark:bg-gray-800 p-1 rounded-lg shadow-md overflow-hidden"
|
||||
class:bg-yellow-50={$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current'}>
|
||||
|
||||
<section
|
||||
class="grow relative bg-gray-100 dark:bg-gray-800 p-1 rounded-lg shadow-md overflow-hidden"
|
||||
class:bg-yellow-50={$journals_loc.entry.edit_kv[
|
||||
$lq__journal_entry_obj?.journal_entry_id
|
||||
] == 'current'}
|
||||
>
|
||||
<div class="absolute top-2 right-2 z-10">
|
||||
<AeCompJournalEntryAiTools
|
||||
content={tmp_entry_obj.content}
|
||||
@@ -406,7 +485,7 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AeCompJournalEntryObjFileLi
|
||||
<AeCompJournalEntryObjFileLi
|
||||
{log_lvl}
|
||||
link_to_type="journal_entry"
|
||||
link_to_id={$lq__journal_entry_obj.journal_entry_id}
|
||||
@@ -421,7 +500,9 @@
|
||||
journal_config={$lq__journal_obj?.cfg_json}
|
||||
mode={modal_mode}
|
||||
on_close={() => (show_append_modal = false)}
|
||||
on_update={() => { show_append_modal = false; }}
|
||||
on_update={() => {
|
||||
show_append_modal = false;
|
||||
}}
|
||||
{log_lvl}
|
||||
/>
|
||||
|
||||
@@ -432,15 +513,23 @@
|
||||
bind:tmp_entry_obj
|
||||
on_save={() => update_journal_entry()}
|
||||
on_force_reset={handle_force_reset}
|
||||
on_show_export={on_show_export}
|
||||
on_append={() => { modal_mode = 'append'; show_append_modal = true; }}
|
||||
on_prepend={() => { modal_mode = 'prepend'; show_append_modal = true; }}
|
||||
{on_show_export}
|
||||
on_append={() => {
|
||||
modal_mode = 'append';
|
||||
show_append_modal = true;
|
||||
}}
|
||||
on_prepend={() => {
|
||||
modal_mode = 'prepend';
|
||||
show_append_modal = true;
|
||||
}}
|
||||
{log_lvl}
|
||||
/>
|
||||
{:else}
|
||||
<div class="p-20 text-center opacity-50 flex flex-col items-center gap-4">
|
||||
<div
|
||||
class="p-20 text-center opacity-50 flex flex-col items-center gap-4"
|
||||
>
|
||||
<Loader2 class="animate-spin" size="4em" />
|
||||
<span class="text-2xl font-bold">Loading Journal Entry...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
show_found_header?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = $bindable(0),
|
||||
lq__journal_obj,
|
||||
let {
|
||||
log_lvl = $bindable(0),
|
||||
lq__journal_obj,
|
||||
lq__journal_entry_obj_li,
|
||||
show_found_header = true
|
||||
}: Props = $props();
|
||||
@@ -68,7 +68,8 @@
|
||||
|
||||
$effect(() => {
|
||||
// Sync local boolean with store ID presence
|
||||
show_append_modal = !!$journals_sess.show__modal_append__journal_entry_id;
|
||||
show_append_modal =
|
||||
!!$journals_sess.show__modal_append__journal_entry_id;
|
||||
});
|
||||
|
||||
function handle_modal_close() {
|
||||
@@ -82,35 +83,42 @@
|
||||
|
||||
// Derived list of visible items (Standardized Search Pattern 2026-01-27)
|
||||
// Ensures count matches exactly what is rendered to the user
|
||||
let visible_journal_entry_obj_li = $derived((() => {
|
||||
// Subscribe to the observable
|
||||
const list = $lq__journal_entry_obj_li;
|
||||
|
||||
// Return null to signify 'loading' vs [] for 'empty'
|
||||
if (list === undefined || list === null) return null;
|
||||
if (!Array.isArray(list)) return [];
|
||||
|
||||
const filtered = list.filter((item: any) => {
|
||||
if (!item) return false;
|
||||
let visible_journal_entry_obj_li = $derived(
|
||||
(() => {
|
||||
// Subscribe to the observable
|
||||
const list = $lq__journal_entry_obj_li;
|
||||
|
||||
// ADMIN/TRUSTED: See everything
|
||||
if ($ae_loc.trusted_access) return true;
|
||||
// Return null to signify 'loading' vs [] for 'empty'
|
||||
if (list === undefined || list === null) return null;
|
||||
if (!Array.isArray(list)) return [];
|
||||
|
||||
// PUBLIC: Filter hidden/disabled
|
||||
// Permissive defaults for missing metadata
|
||||
const is_hidden = item.hide === true || item.hide === 1;
|
||||
const is_disabled = item.enable === false || item.enable === 0;
|
||||
const filtered = list.filter((item: any) => {
|
||||
if (!item) return false;
|
||||
|
||||
return !is_hidden && !is_disabled;
|
||||
});
|
||||
// ADMIN/TRUSTED: See everything
|
||||
if ($ae_loc.trusted_access) return true;
|
||||
|
||||
if (log_lvl) console.log(`visible_journal_entry_obj_li: Input=${list.length}, Output=${filtered.length}`);
|
||||
// PUBLIC: Filter hidden/disabled
|
||||
// Permissive defaults for missing metadata
|
||||
const is_hidden = item.hide === true || item.hide === 1;
|
||||
const is_disabled = item.enable === false || item.enable === 0;
|
||||
|
||||
return filtered;
|
||||
})());
|
||||
return !is_hidden && !is_disabled;
|
||||
});
|
||||
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`visible_journal_entry_obj_li: Input=${list.length}, Output=${filtered.length}`
|
||||
);
|
||||
|
||||
return filtered;
|
||||
})()
|
||||
);
|
||||
</script>
|
||||
|
||||
<section class="journal_list flex flex-col gap-1 md:gap-2 items-center justify-center w-full">
|
||||
<section
|
||||
class="journal_list flex flex-col gap-1 md:gap-2 items-center justify-center w-full"
|
||||
>
|
||||
{#if visible_journal_entry_obj_li === null}
|
||||
<!-- Loading state -->
|
||||
<div class="flex flex-col items-center justify-center p-10 opacity-50">
|
||||
@@ -121,9 +129,16 @@
|
||||
{#if show_found_header}
|
||||
<div class="w-full max-w-(--breakpoint-lg) mb-2">
|
||||
<h2 class="h4 flex items-center gap-2 px-2">
|
||||
<span class="text-sm text-gray-500 font-normal"> Found: </span>
|
||||
<span class="badge preset-tonal-success font-bold text-lg px-3 py-1">
|
||||
{visible_journal_entry_obj_li.length}<span class="text-gray-400 dark:text-gray-600">×</span>
|
||||
<span class="text-sm text-gray-500 font-normal">
|
||||
Found:
|
||||
</span>
|
||||
<span
|
||||
class="badge preset-tonal-success font-bold text-lg px-3 py-1"
|
||||
>
|
||||
{visible_journal_entry_obj_li.length}<span
|
||||
class="text-gray-400 dark:text-gray-600"
|
||||
>×</span
|
||||
>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -147,33 +162,52 @@
|
||||
class:dim={!journals_journal_entry_obj.enable}
|
||||
class:bg-warning-100={!journals_journal_entry_obj?.enable}
|
||||
>
|
||||
<header class="ae_header flex flex-row gap-2 items-center justify-between w-full">
|
||||
<header
|
||||
class="ae_header flex flex-row gap-2 items-center justify-between w-full"
|
||||
>
|
||||
<span class="flex flex-row flex-wrap gap-1">
|
||||
<span class="journal_entry__name *:hover:inline-block">
|
||||
{#if journals_journal_entry_obj.alert}
|
||||
<Siren size="1.25em" class="mx-1 inline-block text-red-500" />
|
||||
<Siren
|
||||
size="1.25em"
|
||||
class="mx-1 inline-block text-red-500"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if journals_journal_entry_obj.priority}
|
||||
<Flag size="1.25em" class="mx-1 inline-block text-yellow-500" />
|
||||
<Flag
|
||||
size="1.25em"
|
||||
class="mx-1 inline-block text-yellow-500"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if journals_journal_entry_obj.group}
|
||||
<Group size="1.25em" class="mx-1 inline-block text-green-500" />
|
||||
<span class="text-xs text-gray-500 hidden">Group:</span>
|
||||
<span class="font-semibold text-sm text-gray-500 hidden md:inline">
|
||||
<Group
|
||||
size="1.25em"
|
||||
class="mx-1 inline-block text-green-500"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 hidden"
|
||||
>Group:</span
|
||||
>
|
||||
<span
|
||||
class="font-semibold text-sm text-gray-500 hidden md:inline"
|
||||
>
|
||||
{journals_journal_entry_obj.group}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<h3 class:dim={journals_journal_entry_obj.hide} class="journal__name h4">
|
||||
<h3
|
||||
class:dim={journals_journal_entry_obj.hide}
|
||||
class="journal__name h4"
|
||||
>
|
||||
{#if journals_journal_entry_obj.template}
|
||||
<NotepadTextDashed
|
||||
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
|
||||
/>
|
||||
|
||||
{@html journals_journal_entry_obj.name ?? '-- no name --'}
|
||||
{@html journals_journal_entry_obj.name ??
|
||||
'-- no name --'}
|
||||
{:else if journals_journal_entry_obj.name}
|
||||
<NotebookText
|
||||
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
|
||||
@@ -190,61 +224,85 @@
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
<span class="flex flex-row flex-wrap gap-1 items-center justify-center">
|
||||
<span
|
||||
class="flex flex-row flex-wrap gap-1 items-center justify-center"
|
||||
>
|
||||
{#if !journals_journal_entry_obj.private}
|
||||
<!-- Button to copy the Markdown version -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
let tmp_entry_obj = journals_journal_entry_obj;
|
||||
let tmp_entry_obj =
|
||||
journals_journal_entry_obj;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(tmp_entry_obj.content)
|
||||
.then(() => {
|
||||
alert('Markdown content copied to clipboard!');
|
||||
alert(
|
||||
'Markdown content copied to clipboard!'
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to copy content:', error);
|
||||
alert('Failed to copy content.');
|
||||
console.error(
|
||||
'Failed to copy content:',
|
||||
error
|
||||
);
|
||||
alert(
|
||||
'Failed to copy content.'
|
||||
);
|
||||
});
|
||||
}}
|
||||
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_plain_md}
|
||||
class:hidden={$lq__journal_obj?.cfg_json
|
||||
?.hide_copy_plain_md}
|
||||
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
|
||||
title="Copy the markdown content"
|
||||
>
|
||||
<RemoveFormatting size="1.25em" />
|
||||
<span class="hidden"> Copy Plaintext Markdown </span>
|
||||
<span class="hidden">
|
||||
Copy Plaintext Markdown
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Button to copy the rendered to HTML version -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
let htmlContent =
|
||||
journals_journal_entry_obj.content_md_html || '';
|
||||
journals_journal_entry_obj.content_md_html ||
|
||||
'';
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(htmlContent)
|
||||
.then(() => {
|
||||
alert('Rendered HTML content copied to clipboard!');
|
||||
alert(
|
||||
'Rendered HTML content copied to clipboard!'
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to copy HTML content:',
|
||||
error
|
||||
);
|
||||
alert('Failed to copy HTML content.');
|
||||
alert(
|
||||
'Failed to copy HTML content.'
|
||||
);
|
||||
});
|
||||
}}
|
||||
class:hidden={journals_journal_entry_obj.template ||
|
||||
$lq__journal_obj?.cfg_json?.hide_copy_html}
|
||||
$lq__journal_obj?.cfg_json
|
||||
?.hide_copy_html}
|
||||
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
|
||||
title="Copy the rendered HTML content"
|
||||
>
|
||||
<CodeXml size="1.25em" />
|
||||
<span class="hidden"> Copy HTML Markup </span>
|
||||
<span class="hidden">
|
||||
Copy HTML Markup
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Button to copy the rich text (rendered HTML) version -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
const element = document.getElementById(
|
||||
`rendered_journal_entry_content_${journals_journal_entry_obj.journal_entry_id}`
|
||||
@@ -253,28 +311,42 @@
|
||||
console.error(
|
||||
'Element not found: rendered_journal_entry_content'
|
||||
);
|
||||
alert('Failed to copy rich content.');
|
||||
alert(
|
||||
'Failed to copy rich content.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlContent = element.innerHTML;
|
||||
const htmlContent =
|
||||
element.innerHTML;
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/html': new Blob([htmlContent], {
|
||||
type: 'text/html'
|
||||
})
|
||||
'text/html': new Blob(
|
||||
[htmlContent],
|
||||
{
|
||||
type: 'text/html'
|
||||
}
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
alert('Rendered rich content copied to clipboard!');
|
||||
alert(
|
||||
'Rendered rich content copied to clipboard!'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy rich content:', error);
|
||||
alert('Failed to copy rich content.');
|
||||
console.error(
|
||||
'Failed to copy rich content:',
|
||||
error
|
||||
);
|
||||
alert(
|
||||
'Failed to copy rich content.'
|
||||
);
|
||||
}
|
||||
}}
|
||||
class:hidden={journals_journal_entry_obj.template ||
|
||||
$lq__journal_obj?.cfg_json?.hide_copy_rich}
|
||||
$lq__journal_obj?.cfg_json
|
||||
?.hide_copy_rich}
|
||||
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
|
||||
title="Copy the rich text (rendered HTML) content"
|
||||
>
|
||||
@@ -283,28 +355,39 @@
|
||||
</button>
|
||||
|
||||
<!-- Clone entry -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
let data_kv = {
|
||||
code: journals_journal_entry_obj.code,
|
||||
category_code: journals_journal_entry_obj.category_code,
|
||||
category_code:
|
||||
journals_journal_entry_obj.category_code,
|
||||
name: journals_journal_entry_obj.name,
|
||||
short_name: journals_journal_entry_obj.short_name,
|
||||
content: journals_journal_entry_obj.content,
|
||||
description: journals_journal_entry_obj.description,
|
||||
short_name:
|
||||
journals_journal_entry_obj.short_name,
|
||||
content:
|
||||
journals_journal_entry_obj.content,
|
||||
description:
|
||||
journals_journal_entry_obj.description,
|
||||
tags: journals_journal_entry_obj.tags
|
||||
};
|
||||
|
||||
journals_func
|
||||
.create_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_id: journals_journal_entry_obj.journal_id,
|
||||
journal_id:
|
||||
journals_journal_entry_obj.journal_id,
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((result) => {
|
||||
if (result?.journal_id_random && result?.journal_entry_id_random) {
|
||||
alert('Journal entry cloned successfully!');
|
||||
if (
|
||||
result?.journal_id_random &&
|
||||
result?.journal_entry_id_random
|
||||
) {
|
||||
alert(
|
||||
'Journal entry cloned successfully!'
|
||||
);
|
||||
goto(
|
||||
`/journals/${result.journal_id_random}/entry/${result.journal_entry_id_random}`
|
||||
);
|
||||
@@ -315,7 +398,9 @@
|
||||
'Error cloning journal entry:',
|
||||
error
|
||||
);
|
||||
alert('Failed to clone journal entry.');
|
||||
alert(
|
||||
'Failed to clone journal entry.'
|
||||
);
|
||||
});
|
||||
}}
|
||||
class:hidden={!journals_journal_entry_obj.template}
|
||||
@@ -330,24 +415,38 @@
|
||||
size="1.25em"
|
||||
class="mx-1 inline-block text-red-400 dark:text-red-600"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 hidden">Private</span>
|
||||
<span class="text-xs text-gray-500 hidden"
|
||||
>Private</span
|
||||
>
|
||||
|
||||
<!-- Button to copy the Markdown version -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
let tmp_entry_obj = journals_journal_entry_obj;
|
||||
let tmp_entry_obj =
|
||||
journals_journal_entry_obj;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(tmp_entry_obj.content_encrypted)
|
||||
.writeText(
|
||||
tmp_entry_obj.content_encrypted
|
||||
)
|
||||
.then(() => {
|
||||
alert('Encrypted content copied to clipboard!');
|
||||
alert(
|
||||
'Encrypted content copied to clipboard!'
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to copy content:', error);
|
||||
alert('Failed to copy content.');
|
||||
console.error(
|
||||
'Failed to copy content:',
|
||||
error
|
||||
);
|
||||
alert(
|
||||
'Failed to copy content.'
|
||||
);
|
||||
});
|
||||
}}
|
||||
class:hidden={$lq__journal_obj?.cfg_json?.hide_copy_encrypted}
|
||||
class:hidden={$lq__journal_obj?.cfg_json
|
||||
?.hide_copy_encrypted}
|
||||
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
|
||||
title="Copy the encrypted content"
|
||||
>
|
||||
@@ -358,18 +457,25 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-2 items-center justify-end">
|
||||
<div
|
||||
class="flex flex-row flex-wrap gap-2 items-center justify-end"
|
||||
>
|
||||
<!-- Linked file count -->
|
||||
<div
|
||||
class="ae_linked_file_count flex flex-row flex-wrap gap-0.5 items-center justify-start"
|
||||
class:hidden={!journals_journal_entry_obj?.data_json?.hosted_file_kv}
|
||||
class:hidden={!journals_journal_entry_obj?.data_json
|
||||
?.hosted_file_kv}
|
||||
>
|
||||
<Files class="mx-1 inline-block" />
|
||||
<span class="text-xs text-gray-500 hidden md:inline">Linked files:</span>
|
||||
<span class="text-xs text-gray-500 hidden md:inline"
|
||||
>Linked files:</span
|
||||
>
|
||||
<span class="font-semibold text-sm text-gray-500">
|
||||
{journals_journal_entry_obj?.data_json?.hosted_file_kv
|
||||
{journals_journal_entry_obj?.data_json
|
||||
?.hosted_file_kv
|
||||
? Object.keys(
|
||||
journals_journal_entry_obj?.data_json?.hosted_file_kv
|
||||
journals_journal_entry_obj?.data_json
|
||||
?.hosted_file_kv
|
||||
).length
|
||||
: 0}×
|
||||
</span>
|
||||
@@ -381,7 +487,10 @@
|
||||
class="tags flex flex-row flex-wrap gap-0.5 items-center justify-start p-1"
|
||||
>
|
||||
<Tags class="mx-1 inline-block" />
|
||||
<span class="text-xs text-gray-500 hidden md:inline">Tags:</span>
|
||||
<span
|
||||
class="text-xs text-gray-500 hidden md:inline"
|
||||
>Tags:</span
|
||||
>
|
||||
|
||||
{#each journals_journal_entry_obj.tags.split(',') as tag}
|
||||
<span
|
||||
@@ -396,27 +505,36 @@
|
||||
|
||||
<!-- Category code for journal entry -->
|
||||
{#if journals_journal_entry_obj.category_code}
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if (
|
||||
$journals_loc.entry.qry__category_code ==
|
||||
$journals_loc.entry
|
||||
.qry__category_code ==
|
||||
journals_journal_entry_obj.category_code
|
||||
) {
|
||||
$journals_loc.entry.qry__category_code = null;
|
||||
$journals_loc.entry.qry__category_code =
|
||||
null;
|
||||
} else {
|
||||
$journals_loc.entry.qry__category_code =
|
||||
journals_journal_entry_obj.category_code;
|
||||
}
|
||||
if ($journals_loc.entry.search_version === undefined) $journals_loc.entry.search_version = 0;
|
||||
if (
|
||||
$journals_loc.entry.search_version ===
|
||||
undefined
|
||||
)
|
||||
$journals_loc.entry.search_version = 0;
|
||||
$journals_loc.entry.search_version++;
|
||||
}}
|
||||
class:bg-green-100={$journals_loc.entry.qry__category_code ==
|
||||
class:bg-green-100={$journals_loc.entry
|
||||
.qry__category_code ==
|
||||
journals_journal_entry_obj.category_code}
|
||||
class="btn btn-sm variant-outline-secondary hover:preset-filled-secondary-500 transition py-1 px-2"
|
||||
title={`Filter by category: ${journals_journal_entry_obj.category_code}`}
|
||||
>
|
||||
<Shapes class="mx-1 inline-block" />
|
||||
{journals_journal_entry_obj.category_code ?? '-- no category --'}
|
||||
{journals_journal_entry_obj.category_code ??
|
||||
'-- no category --'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -434,7 +552,8 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
</a>
|
||||
|
||||
<!-- Button to show a modal that will allow for a quick append to Journal Entry option. -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$journals_sess.show__modal_append__journal_entry_id =
|
||||
journals_journal_entry_obj?.id;
|
||||
@@ -443,7 +562,8 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
);
|
||||
}}
|
||||
class="btn btn-icon btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-secondary-500 transition"
|
||||
title={$lq__journal_obj?.cfg_json?.entry_add_text == 'append'
|
||||
title={$lq__journal_obj?.cfg_json?.entry_add_text ==
|
||||
'append'
|
||||
? 'Append to Journal Entry'
|
||||
: 'Prepend to Journal Entry'}
|
||||
>
|
||||
@@ -456,11 +576,14 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
<div
|
||||
class:hidden={journals_journal_entry_obj.hide ||
|
||||
(journals_journal_entry_obj.private &&
|
||||
$journals_slct.journal_obj?.cfg_json.hide_private) ||
|
||||
$journals_slct.journal_obj?.cfg_json
|
||||
.hide_private) ||
|
||||
(journals_journal_entry_obj.personal &&
|
||||
$journals_slct.journal_obj?.cfg_json.hide_personal) ||
|
||||
$journals_slct.journal_obj?.cfg_json
|
||||
.hide_personal) ||
|
||||
(journals_journal_entry_obj.professional &&
|
||||
$journals_slct.journal_obj?.cfg_json.hide_professional)}
|
||||
$journals_slct.journal_obj?.cfg_json
|
||||
.hide_professional)}
|
||||
class="journal__content
|
||||
w-full p-1
|
||||
bg-slate-100 text-gray-900
|
||||
@@ -479,11 +602,13 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
? `${$journals_slct.journal_obj.cfg_json.entry_li_max_height}`
|
||||
: ''}
|
||||
|
||||
{$journals_slct.journal_obj.cfg_json.entry_li_click_max_height
|
||||
{$journals_slct.journal_obj.cfg_json
|
||||
.entry_li_click_max_height
|
||||
? `${$journals_slct.journal_obj.cfg_json.entry_li_click_max_height}`
|
||||
: ''}
|
||||
|
||||
{$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height
|
||||
{$journals_slct.journal_obj.cfg_json
|
||||
.entry_li_hover_max_height
|
||||
? `${$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height}`
|
||||
: ''}
|
||||
"
|
||||
@@ -509,9 +634,11 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
class:hidden={!journals_journal_entry_obj?.original_datetime &&
|
||||
!journals_journal_entry_obj?.original_timezone}
|
||||
>
|
||||
<span class="ae_label text-sm">Original date/time:</span>
|
||||
<span class="ae_label text-sm">Original date/time:</span
|
||||
>
|
||||
{#if journals_journal_entry_obj.original_datetime}
|
||||
<span class="ae_value ae_prop prop_original_datetime font-semibold"
|
||||
<span
|
||||
class="ae_value ae_prop prop_original_datetime font-semibold"
|
||||
>{ae_util.iso_datetime_formatter(
|
||||
journals_journal_entry_obj.original_datetime,
|
||||
'datetime_12_long'
|
||||
@@ -531,7 +658,8 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
class="ae_meta mt-2 flex flex-col sm:flex-row gap-2 items-center justify-center text-xs text-gray-500"
|
||||
>
|
||||
<span
|
||||
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode}
|
||||
class:hidden={!$ae_loc.trusted_access ||
|
||||
!$ae_loc.edit_mode}
|
||||
class="flex flex-row gap-1 items-center justify-center"
|
||||
>
|
||||
<span class="journal_entry__created_on">
|
||||
@@ -554,22 +682,28 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
</span>
|
||||
|
||||
<!-- Set/unset hide (boolean) -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
let data_kv = {
|
||||
hide: journals_journal_entry_obj?.hide ? false : true
|
||||
hide: journals_journal_entry_obj?.hide
|
||||
? false
|
||||
: true
|
||||
};
|
||||
journals_func
|
||||
.update_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: journals_journal_entry_obj.journal_entry_id,
|
||||
journal_entry_id:
|
||||
journals_journal_entry_obj.journal_entry_id,
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then(() => {
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error('Error updating journal entry:', error);
|
||||
console.error(
|
||||
'Error updating journal entry:',
|
||||
error
|
||||
);
|
||||
alert('Failed to update journal entry.');
|
||||
});
|
||||
}}
|
||||
@@ -609,9 +743,14 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center p-10 opacity-50 text-center">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-10 opacity-50 text-center"
|
||||
>
|
||||
<BookOpenText size="3em" class="mb-2 opacity-20 mx-auto" />
|
||||
<p>No Journal Entry available to show. Please check the query filters or create a new Entry.</p>
|
||||
<p>
|
||||
No Journal Entry available to show. Please check the query
|
||||
filters or create a new Entry.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
</script>
|
||||
|
||||
{#if $lq__journal_entry_obj_li}
|
||||
<Journal_entry_obj_li
|
||||
{lq__journal_obj}
|
||||
{lq__journal_entry_obj_li}
|
||||
<Journal_entry_obj_li
|
||||
{lq__journal_obj}
|
||||
{lq__journal_entry_obj_li}
|
||||
{show_found_header}
|
||||
{log_lvl}
|
||||
/>
|
||||
|
||||
@@ -76,25 +76,29 @@
|
||||
$journals_loc.entry.search_version++;
|
||||
}
|
||||
|
||||
function preventDefault<T extends Event>(fn: (event: T) => void) {
|
||||
function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
return function (event: T) {
|
||||
event.preventDefault();
|
||||
event.prevent_default();
|
||||
fn(event);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2">
|
||||
<div
|
||||
class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2"
|
||||
>
|
||||
<!-- Search input form -->
|
||||
<span class="flex flex-row flex-wrap items-center justify-center gap-1">
|
||||
<form
|
||||
onsubmit={preventDefault(() => {
|
||||
onsubmit={prevent_default(() => {
|
||||
handle_search_trigger();
|
||||
})}
|
||||
autocomplete="off"
|
||||
class="search_form flex flex-row flex-wrap gap-1 items-center justify-center"
|
||||
>
|
||||
<span class="text-sm text-gray-500 hidden lg:inline"> Search: </span>
|
||||
<span class="text-sm text-gray-500 hidden lg:inline">
|
||||
Search:
|
||||
</span>
|
||||
<input
|
||||
disabled={false}
|
||||
type="text"
|
||||
@@ -113,7 +117,7 @@
|
||||
class:bg-red-200={$journals_sess.entry_li == null}
|
||||
class:dark:bg-red-800={$journals_sess.entry_li == null}
|
||||
/>
|
||||
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm preset-tonal-primary border border-primary-500 hover:preset-filled-primary-500 transition"
|
||||
@@ -125,7 +129,8 @@
|
||||
<!-- Clear search text button -->
|
||||
<button
|
||||
type="button"
|
||||
class:hidden={!$journals_loc.entry.qry__search_text && !$journals_loc.entry.qry__category_code}
|
||||
class:hidden={!$journals_loc.entry.qry__search_text &&
|
||||
!$journals_loc.entry.qry__category_code}
|
||||
onclick={() => {
|
||||
$journals_loc.entry.qry__search_text = '';
|
||||
$journals_loc.entry.qry__category_code = '';
|
||||
@@ -140,7 +145,10 @@
|
||||
"
|
||||
title="Clear search query text"
|
||||
>
|
||||
<RemoveFormatting size="1.25em" class="text-neutral-800/60 dark:text-neutral-50/60" />
|
||||
<RemoveFormatting
|
||||
size="1.25em"
|
||||
class="text-neutral-800/60 dark:text-neutral-50/60"
|
||||
/>
|
||||
<span class="hidden md:inline"> Clear </span>
|
||||
</button>
|
||||
</form>
|
||||
@@ -171,7 +179,9 @@
|
||||
</span>
|
||||
|
||||
<!-- Search Control Toggles -->
|
||||
<span class="flex flex-row flex-wrap items-center gap-2 border-l border-surface-300-700 pl-2">
|
||||
<span
|
||||
class="flex flex-row flex-wrap items-center gap-2 border-l border-surface-300-700 pl-2"
|
||||
>
|
||||
<!-- Global Search hidden until backend supports person_id in entries 2026-01-27 -->
|
||||
<!--
|
||||
<label
|
||||
@@ -193,7 +203,9 @@
|
||||
class="flex items-center gap-1 cursor-pointer"
|
||||
title="When enabled, search results are fetched directly from the server first."
|
||||
>
|
||||
<span class="text-xs font-semibold text-gray-500"> Remote First? </span>
|
||||
<span class="text-xs font-semibold text-gray-500">
|
||||
Remote First?
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$journals_loc.entry.qry__remote_first}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api/api';
|
||||
import { ae_api } from '$lib/stores/ae_stores';
|
||||
import { journals_slct, journals_loc, journals_trig } from '$lib/ae_journals/ae_journals_stores';
|
||||
import {
|
||||
journals_slct,
|
||||
journals_loc,
|
||||
journals_trig
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
import { BookType } from 'lucide-svelte';
|
||||
|
||||
|
||||
// Props
|
||||
let {
|
||||
class: className = "",
|
||||
placeholder = "Type your quick note... (First line = Title)",
|
||||
let {
|
||||
class: className = '',
|
||||
placeholder = 'Type your quick note... (First line = Title)',
|
||||
journals_li = [] // Optional list of journals to select from
|
||||
} = $props();
|
||||
|
||||
// State
|
||||
let note_content = $state("");
|
||||
let note_content = $state('');
|
||||
let is_submitting = $state(false);
|
||||
|
||||
// Derived / Local target
|
||||
@@ -21,12 +25,14 @@
|
||||
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
|
||||
let target_journal_id = $derived($journals_slct.journal_id || selected_journal_id);
|
||||
let target_journal_id = $derived(
|
||||
$journals_slct.journal_id || selected_journal_id
|
||||
);
|
||||
|
||||
async function handle_submit() {
|
||||
if (!note_content.trim()) return;
|
||||
if (!target_journal_id) {
|
||||
alert("Please select a target journal first.");
|
||||
alert('Please select a target journal first.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,8 +40,8 @@
|
||||
|
||||
const lines = note_content.trim().split('\n');
|
||||
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
|
||||
const entry_content = lines.slice(1).join('\n').trim();
|
||||
|
||||
@@ -57,15 +63,15 @@
|
||||
});
|
||||
|
||||
if (res) {
|
||||
note_content = "";
|
||||
note_content = '';
|
||||
// Trigger refresh
|
||||
$journals_trig.journal_entry_li = true;
|
||||
} else {
|
||||
alert("Failed to create note.");
|
||||
alert('Failed to create note.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating journal entry:", error);
|
||||
alert("Failed to create note.");
|
||||
console.error('Error creating journal entry:', error);
|
||||
alert('Failed to create note.');
|
||||
}
|
||||
|
||||
is_submitting = false;
|
||||
@@ -85,20 +91,24 @@
|
||||
</script>
|
||||
|
||||
<div class="card p-4 space-y-4 variant-filled-surface {className}">
|
||||
<header class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
|
||||
<header
|
||||
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2"
|
||||
>
|
||||
<h3 class="h3 flex items-center gap-2">
|
||||
<BookType size="1.2em" class="text-primary-500" />
|
||||
Quick Add
|
||||
</h3>
|
||||
|
||||
|
||||
{#if journals_li && journals_li.length > 0}
|
||||
<div class="w-full sm:w-auto">
|
||||
<select
|
||||
<select
|
||||
class="select select-sm variant-filled-surface border-surface-500/30 font-bold"
|
||||
value={target_journal_id}
|
||||
onchange={handle_journal_change}
|
||||
>
|
||||
<option value="" disabled selected={!target_journal_id}>Select Target Journal...</option>
|
||||
<option value="" disabled selected={!target_journal_id}
|
||||
>Select Target Journal...</option
|
||||
>
|
||||
{#each journals_li as journal}
|
||||
<option value={journal.id}>{journal.name}</option>
|
||||
{/each}
|
||||
@@ -108,8 +118,8 @@
|
||||
<span class="badge variant-filled-error">No Journal Selected</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<textarea
|
||||
|
||||
<textarea
|
||||
class="textarea variant-filled-surface border-surface-500/20"
|
||||
rows="3"
|
||||
bind:value={note_content}
|
||||
@@ -119,26 +129,30 @@
|
||||
></textarea>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-[10px] opacity-50 font-mono uppercase tracking-tighter hidden sm:block">
|
||||
<span
|
||||
class="text-[10px] opacity-50 font-mono uppercase tracking-tighter hidden sm:block"
|
||||
>
|
||||
Press Ctrl + Enter to save
|
||||
</span>
|
||||
<div class="flex justify-end space-x-2 grow sm:grow-0">
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
onclick={() => note_content = ""}
|
||||
onclick={() => (note_content = '')}
|
||||
disabled={is_submitting || note_content.length === 0}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-primary font-bold shadow-md"
|
||||
onclick={handle_submit}
|
||||
disabled={is_submitting || !target_journal_id || note_content.length === 0}
|
||||
disabled={is_submitting ||
|
||||
!target_journal_id ||
|
||||
note_content.length === 0}
|
||||
>
|
||||
{#if is_submitting}Saving...{:else}Add Note{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,10 +44,7 @@
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
// *** 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 {
|
||||
journals_loc,
|
||||
journals_sess,
|
||||
@@ -75,7 +72,8 @@
|
||||
}: Props = $props();
|
||||
|
||||
// *** Internal State
|
||||
let tab: 'actions' | 'general' | 'security' | 'ui' | 'json' = $state('actions');
|
||||
let tab: 'actions' | 'general' | 'security' | 'ui' | 'json' =
|
||||
$state('actions');
|
||||
let tmp__journal_obj: any = $state({});
|
||||
|
||||
// Deep copy on mount or when lq changes to ensure we have a working copy
|
||||
@@ -83,10 +81,16 @@
|
||||
if (show && $lq__journal_obj) {
|
||||
const source_id = $lq__journal_obj.journal_id;
|
||||
untrack(() => {
|
||||
if (!tmp__journal_obj.journal_id || tmp__journal_obj.journal_id !== source_id) {
|
||||
tmp__journal_obj = JSON.parse(JSON.stringify($lq__journal_obj));
|
||||
if (
|
||||
!tmp__journal_obj.journal_id ||
|
||||
tmp__journal_obj.journal_id !== source_id
|
||||
) {
|
||||
tmp__journal_obj = JSON.parse(
|
||||
JSON.stringify($lq__journal_obj)
|
||||
);
|
||||
// Ensure cfg_json exists
|
||||
if (!tmp__journal_obj.cfg_json) tmp__journal_obj.cfg_json = {};
|
||||
if (!tmp__journal_obj.cfg_json)
|
||||
tmp__journal_obj.cfg_json = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -136,7 +140,7 @@
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
|
||||
if (close_modal) show = false;
|
||||
} catch (error) {
|
||||
console.error('Error updating journal:', error);
|
||||
@@ -145,7 +149,11 @@
|
||||
}
|
||||
|
||||
async function delete_journal() {
|
||||
if (confirm(`CRITICAL WARNING: Are you sure you want to delete the journal "${$lq__journal_obj.name}"? This will delete all entries associated with it.`)) {
|
||||
if (
|
||||
confirm(
|
||||
`CRITICAL WARNING: Are you sure you want to delete the journal "${$lq__journal_obj.name}"? This will delete all entries associated with it.`
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await journals_func.delete_ae_obj_id__journal({
|
||||
api_cfg: $ae_api,
|
||||
@@ -180,20 +188,52 @@
|
||||
|
||||
<div class="space-y-6 py-2 h-[75vh] overflow-y-auto px-4">
|
||||
<!-- Navigation Tabs -->
|
||||
<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">
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'actions' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'actions')}>
|
||||
<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"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'actions'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'actions')}
|
||||
>
|
||||
<Zap size="1.1em" class="mr-1" /> Quick Actions
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'general' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'general')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'general'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'general')}
|
||||
>
|
||||
<Layout size="1.1em" class="mr-1" /> General
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'security' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'security')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'security'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'security')}
|
||||
>
|
||||
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'ui' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'ui')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'ui'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'ui')}
|
||||
>
|
||||
<Palette size="1.1em" class="mr-1" /> UI/Visuals
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'json' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'json')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'json'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'json')}
|
||||
>
|
||||
<CodeXml size="1.1em" class="mr-1" /> JSON
|
||||
</button>
|
||||
</div>
|
||||
@@ -201,111 +241,241 @@
|
||||
{#if tab === 'actions'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button type="button" class="btn variant-soft-secondary w-full py-4 text-lg font-bold" onclick={() => { show = false; on_new_entry?.(); }}>
|
||||
<FilePlus size="1.5em" class="mr-2"/> New Journal Entry
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-secondary w-full py-4 text-lg font-bold"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_new_entry?.();
|
||||
}}
|
||||
>
|
||||
<FilePlus size="1.5em" class="mr-2" /> New Journal Entry
|
||||
</button>
|
||||
<button type="button" class="btn variant-soft-surface w-full py-4" onclick={() => { show = false; on_show_export?.(); }}>
|
||||
<FileDown size="1.5em" class="mr-2"/> Export Entries
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface w-full py-4"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_show_export?.();
|
||||
}}
|
||||
>
|
||||
<FileDown size="1.5em" class="mr-2" /> Export Entries
|
||||
</button>
|
||||
<button type="button" class="btn variant-soft-surface w-full py-4" onclick={() => { show = false; on_show_import?.(); }}>
|
||||
<FileUp size="1.5em" class="mr-2"/> Import Entries
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface w-full py-4"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_show_import?.();
|
||||
}}
|
||||
>
|
||||
<FileUp size="1.5em" class="mr-2" /> Import Entries
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'general'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<!-- Core Meta -->
|
||||
<section class="grid grid-cols-1 gap-4 p-2">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Journal Name</span>
|
||||
<input type="text" bind:value={tmp__journal_obj.name} class="input variant-form-material" placeholder="e.g. Personal Log" />
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Journal Name</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={tmp__journal_obj.name}
|
||||
class="input variant-form-material"
|
||||
placeholder="e.g. Personal Log"
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Description (Markdown)</span>
|
||||
<textarea bind:value={tmp__journal_obj.description} class="textarea variant-form-material h-32" placeholder="Describe the purpose of this journal..."></textarea>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Description (Markdown)</span
|
||||
>
|
||||
<textarea
|
||||
bind:value={tmp__journal_obj.description}
|
||||
class="textarea variant-form-material h-32"
|
||||
placeholder="Describe the purpose of this journal..."
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Type</span>
|
||||
<select bind:value={tmp__journal_obj.type_code} class="select variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Type</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp__journal_obj.type_code}
|
||||
class="select variant-form-material"
|
||||
>
|
||||
{#each $journals_loc.journal.type_code_li as type}
|
||||
<option value={type.code}>{type.name}</option>
|
||||
<option value={type.code}
|
||||
>{type.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Sort Group</span>
|
||||
<input type="text" bind:value={tmp__journal_obj.group} class="input variant-form-material" placeholder="Standard" />
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Sort Group</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={tmp__journal_obj.group}
|
||||
class="input variant-form-material"
|
||||
placeholder="Standard"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Categories -->
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<MonitorPlay size="1em" class="text-primary-500" />
|
||||
Journal Categories
|
||||
</h2>
|
||||
<div class="space-y-2 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10">
|
||||
<div
|
||||
class="space-y-2 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10"
|
||||
>
|
||||
{#each tmp__journal_obj?.cfg_json?.category_li ?? [] as category, i}
|
||||
<div class="flex gap-2 items-center">
|
||||
<input type="text" bind:value={category.code} class="input input-sm w-32" placeholder="Code" />
|
||||
<input type="text" bind:value={category.name} class="input input-sm grow" placeholder="Display Name" />
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-error" onclick={() => { tmp__journal_obj.cfg_json.category_li.splice(i, 1); }}>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={category.code}
|
||||
class="input input-sm w-32"
|
||||
placeholder="Code"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={category.name}
|
||||
class="input input-sm grow"
|
||||
placeholder="Display Name"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-error"
|
||||
onclick={() => {
|
||||
tmp__journal_obj.cfg_json.category_li.splice(
|
||||
i,
|
||||
1
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Minus size="1em" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button type="button" class="btn btn-sm variant-soft-primary w-full mt-2" onclick={() => {
|
||||
if(!tmp__journal_obj.cfg_json.category_li) tmp__journal_obj.cfg_json.category_li = [];
|
||||
tmp__journal_obj.cfg_json.category_li.push({code: '', name: ''});
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-primary w-full mt-2"
|
||||
onclick={() => {
|
||||
if (!tmp__journal_obj.cfg_json.category_li)
|
||||
tmp__journal_obj.cfg_json.category_li = [];
|
||||
tmp__journal_obj.cfg_json.category_li.push({
|
||||
code: '',
|
||||
name: ''
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus size="1em" class="mr-1" /> Add Category
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'security'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<!-- Status & Lifecycle -->
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Fingerprint size="1.2em" class="text-primary-500" />
|
||||
Status & Lifecycle
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.enable} onchange={() => handle_update_journal(false)} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp__journal_obj.enable}
|
||||
onchange={() => handle_update_journal(false)}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Enabled</span>
|
||||
<span class="text-xs opacity-60">Allow access to this journal</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Allow access to this journal</span
|
||||
>
|
||||
</div>
|
||||
</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">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.hide} onchange={() => handle_update_journal(false)} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp__journal_obj.hide}
|
||||
onchange={() => handle_update_journal(false)}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Hidden</span>
|
||||
<span class="text-xs opacity-60">Hide from standard lists</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Hide from standard lists</span
|
||||
>
|
||||
</div>
|
||||
</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">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.priority} onchange={() => handle_update_journal(false)} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp__journal_obj.priority}
|
||||
onchange={() => handle_update_journal(false)}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Priority Journal</span>
|
||||
<span class="text-xs opacity-60">Star or pin to top</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Star or pin to top</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10">
|
||||
<div
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-sm">Sort Order</span>
|
||||
<span class="text-xs opacity-60">Manual list position</span>
|
||||
<span class="font-bold text-sm">Sort Order</span
|
||||
>
|
||||
<span class="text-xs opacity-60"
|
||||
>Manual list position</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp__journal_obj.sort = (tmp__journal_obj.sort ?? 0) - 1; handle_update_journal(false); }}><Minus size="1em"/></button>
|
||||
<span class="font-mono font-bold w-8 text-center text-lg">{tmp__journal_obj.sort ?? 0}</span>
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp__journal_obj.sort = (tmp__journal_obj.sort ?? 0) + 1; handle_update_journal(false); }}><Plus size="1em"/></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp__journal_obj.sort =
|
||||
(tmp__journal_obj.sort ?? 0) - 1;
|
||||
handle_update_journal(false);
|
||||
}}><Minus size="1em" /></button
|
||||
>
|
||||
<span
|
||||
class="font-mono font-bold w-8 text-center text-lg"
|
||||
>{tmp__journal_obj.sort ?? 0}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp__journal_obj.sort =
|
||||
(tmp__journal_obj.sort ?? 0) + 1;
|
||||
handle_update_journal(false);
|
||||
}}><Plus size="1em" /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,64 +483,126 @@
|
||||
|
||||
<!-- Encryption Passcodes -->
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<LockKeyhole size="1.2em" class="text-primary-500" />
|
||||
Encryption Passcodes
|
||||
</h2>
|
||||
<div class="bg-warning-500/10 border border-warning-500/30 p-4 rounded-lg space-y-4 shadow-inner">
|
||||
<div
|
||||
class="bg-warning-500/10 border border-warning-500/30 p-4 rounded-lg space-y-4 shadow-inner"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold uppercase tracking-wider opacity-70">Primary Passcode (Stored)</span>
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider opacity-70"
|
||||
>Primary Passcode (Stored)</span
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input type="password" bind:value={tmp__journal_obj.passcode} class="input variant-form-material grow" placeholder="Module-level passcode" />
|
||||
<input
|
||||
type="password"
|
||||
bind:value={tmp__journal_obj.passcode}
|
||||
class="input variant-form-material grow"
|
||||
placeholder="Module-level passcode"
|
||||
/>
|
||||
<Fingerprint class="opacity-30" />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold uppercase tracking-wider opacity-70">Private Passcode (Double Encryption)</span>
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider opacity-70"
|
||||
>Private Passcode (Double Encryption)</span
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input type="password" bind:value={tmp__journal_obj.private_passcode} class="input variant-form-material grow" placeholder="User-level secret" />
|
||||
<input
|
||||
type="password"
|
||||
bind:value={
|
||||
tmp__journal_obj.private_passcode
|
||||
}
|
||||
class="input variant-form-material grow"
|
||||
placeholder="User-level secret"
|
||||
/>
|
||||
<ShieldCheck class="opacity-30" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="text-[10px] text-warning-700 dark:text-warning-300 italic">
|
||||
* Note: Passcodes are used locally for E2EE. Primary is often stored in the DB, while Private should remain known only to you.
|
||||
<div
|
||||
class="text-[10px] text-warning-700 dark:text-warning-300 italic"
|
||||
>
|
||||
* Note: Passcodes are used locally for E2EE. Primary
|
||||
is often stored in the DB, while Private should
|
||||
remain known only to you.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pt-8">
|
||||
<button type="button" class="btn btn-sm variant-filled-error w-full shadow-lg" onclick={delete_journal}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-error w-full shadow-lg"
|
||||
onclick={delete_journal}
|
||||
>
|
||||
<Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'ui'}
|
||||
<div class="space-y-8 animate-in fade-in duration-300">
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<MonitorPlay size="1.2em" class="text-primary-500" />
|
||||
Default View Modes
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10">
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Preferred Viewer</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.pref_viewer} class="select variant-form-material">
|
||||
<option value="rendered">Rendered HTML (Default)</option>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Preferred Viewer</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json.pref_viewer
|
||||
}
|
||||
class="select variant-form-material"
|
||||
>
|
||||
<option value="rendered"
|
||||
>Rendered HTML (Default)</option
|
||||
>
|
||||
<option value="plain">Plain Text</option>
|
||||
<option value="codemirror">CodeMirror (Syntax)</option>
|
||||
<option value="codemirror"
|
||||
>CodeMirror (Syntax)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Preferred Editor</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.pref_editor} class="select variant-form-material">
|
||||
<option value="textarea">Standard Textarea</option>
|
||||
<option value="codemirror">CodeMirror (Advanced)</option>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Preferred Editor</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json.pref_editor
|
||||
}
|
||||
class="select variant-form-material"
|
||||
>
|
||||
<option value="textarea"
|
||||
>Standard Textarea</option
|
||||
>
|
||||
<option value="codemirror"
|
||||
>CodeMirror (Advanced)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Color Scheme</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.color_scheme} class="select variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Color Scheme</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json.color_scheme
|
||||
}
|
||||
class="select variant-form-material"
|
||||
>
|
||||
<option value="">Default (Slate)</option>
|
||||
<option value="blue">Deep Blue</option>
|
||||
<option value="green">Nature Green</option>
|
||||
@@ -380,10 +612,20 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Quick Add Placement</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.entry_add_text} class="select variant-form-material">
|
||||
<option value="append">Append to End (Default)</option>
|
||||
<option value="prepend">Prepend to Start</option>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Quick Add Placement</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json.entry_add_text
|
||||
}
|
||||
class="select variant-form-material"
|
||||
>
|
||||
<option value="append"
|
||||
>Append to End (Default)</option
|
||||
>
|
||||
<option value="prepend">Prepend to Start</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -391,106 +633,252 @@
|
||||
|
||||
<!-- Visibility Toggles (Restored) -->
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Eye size="1.2em" class="text-primary-500" />
|
||||
Entry Visibility (Global)
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_private} class="checkbox checkbox-sm" />
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2"
|
||||
>
|
||||
<label
|
||||
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_private
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-xs font-bold">Hide Private</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_personal} class="checkbox checkbox-sm" />
|
||||
<label
|
||||
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_personal
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-xs font-bold">Hide Personal</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_professional} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs font-bold">Hide Professional</span>
|
||||
<label
|
||||
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_professional
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-xs font-bold"
|
||||
>Hide Professional</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Button Toggles (Restored) -->
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Settings size="1.2em" class="text-primary-500" />
|
||||
Entry Feature Buttons
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_alert} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Alert</span>
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2"
|
||||
>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_btn_alert
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Alert</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_private} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Private</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_btn_private
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Private</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_public} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Public</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_btn_public
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Public</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_personal} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Pers.</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_btn_personal
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Pers.</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_professional} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Prof.</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json
|
||||
.hide_btn_professional
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Prof.</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_template} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Templ.</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_btn_template
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Templ.</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_copy_plain_md} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Copy MD</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_copy_plain_md
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Copy MD</span
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_clone} class="checkbox checkbox-sm" />
|
||||
<span class="text-[10px] uppercase font-bold">Hide Clone</span>
|
||||
<label
|
||||
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
tmp__journal_obj.cfg_json.hide_clone
|
||||
}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-[10px] uppercase font-bold"
|
||||
>Hide Clone</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4 p-2">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Layout size="1.2em" class="text-primary-500" />
|
||||
List Interactions
|
||||
</h2>
|
||||
<div class="bg-surface-500/5 p-4 rounded-lg space-y-4">
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70">Expansion Trigger</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.expand_li_content} class="select select-sm variant-form-material">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>Expansion Trigger</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json.expand_li_content
|
||||
}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="click">Click to Expand</option>
|
||||
<option value="hover">Hover to Expand</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70">List Max Height</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.entry_li_max_height} class="select select-sm variant-form-material">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>List Max Height</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json
|
||||
.entry_li_max_height
|
||||
}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
<option value="max-h-16">Small (16)</option>
|
||||
<option value="max-h-32">Medium (32)</option>
|
||||
<option value="max-h-32">Medium (32)</option
|
||||
>
|
||||
<option value="max-h-64">Large (64)</option>
|
||||
<option value="max-h-full">Full</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70">Active Max Height</span>
|
||||
<select bind:value={tmp__journal_obj.cfg_json.entry_li_click_max_height} class="select select-sm variant-form-material">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>Active Max Height</span
|
||||
>
|
||||
<select
|
||||
bind:value={
|
||||
tmp__journal_obj.cfg_json
|
||||
.entry_li_click_max_height
|
||||
}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
<option value="active:max-h-64">Large (64)</option>
|
||||
<option value="active:max-h-96">X-Large (96)</option>
|
||||
<option value="active:max-h-full">Full</option>
|
||||
<option value="active:max-h-64"
|
||||
>Large (64)</option
|
||||
>
|
||||
<option value="active:max-h-96"
|
||||
>X-Large (96)</option
|
||||
>
|
||||
<option value="active:max-h-full"
|
||||
>Full</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'json'}
|
||||
<div class="h-full min-h-[400px]">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
@@ -505,12 +893,20 @@
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex gap-4">
|
||||
<button type="button" class="btn variant-ghost-surface font-bold min-w-[100px]" onclick={() => (show = false)}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-ghost-surface font-bold min-w-[100px]"
|
||||
onclick={() => (show = false)}
|
||||
>
|
||||
<X size="1.2em" class="mr-2" /> Cancel
|
||||
</button>
|
||||
<button type="button" class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]" onclick={() => handle_update_journal(true)}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]"
|
||||
onclick={() => handle_update_journal(true)}
|
||||
>
|
||||
<Check size="1.2em" class="mr-2" /> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -3,7 +3,17 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
// *** Import other supporting libraries
|
||||
import { BookPlus, BookOpenText, FilePlus, Menu, Pencil, FileDown, FileUp, Settings, LoaderCircle } from 'lucide-svelte';
|
||||
import {
|
||||
BookPlus,
|
||||
BookOpenText,
|
||||
FilePlus,
|
||||
Menu,
|
||||
Pencil,
|
||||
FileDown,
|
||||
FileUp,
|
||||
Settings,
|
||||
LoaderCircle
|
||||
} from 'lucide-svelte';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
@@ -34,7 +44,13 @@
|
||||
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 {
|
||||
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 = {};
|
||||
@@ -60,7 +76,8 @@
|
||||
if (
|
||||
$lq__journal_obj?.id &&
|
||||
$journals_sess?.journal_kv[$lq__journal_obj?.id] &&
|
||||
$journals_sess?.journal_kv[$lq__journal_obj?.id]?.journal_passcode_verified
|
||||
$journals_sess?.journal_kv[$lq__journal_obj?.id]
|
||||
?.journal_passcode_verified
|
||||
) {
|
||||
if (passcode_timer) {
|
||||
if (log_lvl) {
|
||||
@@ -74,27 +91,30 @@
|
||||
const timeout_ms = 1000 * 60 * timeout_minutes;
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Setting passcode timer for ${timeout_minutes} minutes (${timeout_ms}ms)`);
|
||||
console.log(
|
||||
`Setting passcode timer for ${timeout_minutes} minutes (${timeout_ms}ms)`
|
||||
);
|
||||
}
|
||||
|
||||
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] = {};
|
||||
}
|
||||
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;
|
||||
// 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
|
||||
);
|
||||
passcode_timer = null;
|
||||
}, timeout_ms);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,9 +157,13 @@
|
||||
});
|
||||
|
||||
if (results?.journal_entry_id_random) {
|
||||
$journals_slct.journal_entry_id = results.journal_entry_id_random;
|
||||
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] = 'current';
|
||||
goto(`/journals/${$lq__journal_obj?.journal_id}/entry/${results.journal_entry_id_random}`);
|
||||
$journals_slct.journal_entry_id =
|
||||
results.journal_entry_id_random;
|
||||
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] =
|
||||
'current';
|
||||
goto(
|
||||
`/journals/${$lq__journal_obj?.journal_id}/entry/${results.journal_entry_id_random}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating journal entry:', error);
|
||||
@@ -169,19 +193,28 @@
|
||||
class="badge preset-tonal-success font-bold text-lg px-2 ml-2"
|
||||
title="Entries matching current filters"
|
||||
>
|
||||
{$lq__journal_entry_obj_li?.length ?? '0'}<span class="text-xs opacity-50 ml-0.5">×</span>
|
||||
{$lq__journal_entry_obj_li?.length ?? '0'}<span
|
||||
class="text-xs opacity-50 ml-0.5">×</span
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#await $journals_prom.load__journal_entry_obj_li}
|
||||
<LoaderCircle size="1em" class="inline-block animate-spin ml-1 text-primary-500" />
|
||||
<LoaderCircle
|
||||
size="1em"
|
||||
class="inline-block animate-spin ml-1 text-primary-500"
|
||||
/>
|
||||
{/await}
|
||||
</h2>
|
||||
|
||||
<div class="grow flex flex-row flex-wrap gap-2 items-center justify-end">
|
||||
<div
|
||||
class="grow flex flex-row flex-wrap gap-2 items-center justify-end"
|
||||
>
|
||||
<!-- Simplified Config Trigger -->
|
||||
<button type="button"
|
||||
onclick={() => ($journals_sess.show__modal_edit__journal_obj = true)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
($journals_sess.show__modal_edit__journal_obj = true)}
|
||||
class="btn variant-filled-secondary py-1 px-3 shadow-md"
|
||||
title="Journal Config & Actions"
|
||||
>
|
||||
@@ -238,6 +271,6 @@
|
||||
{lq__journal_obj}
|
||||
show={$journals_sess.show__modal_edit__journal_obj}
|
||||
on_new_entry={handle_new_entry}
|
||||
on_show_export={on_show_export}
|
||||
on_show_import={on_show_import}
|
||||
{on_show_export}
|
||||
{on_show_import}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
* Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop)
|
||||
* Style: Tailwind 4 + Skeleton UI Reference Standard
|
||||
*/
|
||||
import { BookOpenText, BookType, Hash, Calendar, Clock } from 'lucide-svelte';
|
||||
import {
|
||||
BookOpenText,
|
||||
BookType,
|
||||
Hash,
|
||||
Calendar,
|
||||
Clock
|
||||
} from 'lucide-svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
|
||||
@@ -17,7 +23,9 @@
|
||||
</script>
|
||||
|
||||
<!-- Responsive Grid Container -->
|
||||
<section class="journal_list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
|
||||
<section
|
||||
class="journal_list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4"
|
||||
>
|
||||
{#if $lq__journal_obj_li && $lq__journal_obj_li.length}
|
||||
{#each $lq__journal_obj_li as journal, index}
|
||||
<div
|
||||
@@ -25,12 +33,13 @@
|
||||
journal_card
|
||||
group relative
|
||||
flex flex-col justify-between
|
||||
bg-surface-50 dark:bg-surface-900
|
||||
bg-surface-50 dark:bg-surface-900
|
||||
border border-surface-500/20 rounded-xl
|
||||
p-5 shadow-sm hover:shadow-xl hover:border-primary-500/50
|
||||
transition-all duration-200 ease-in-out
|
||||
"
|
||||
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:border-warning-500={!journal?.enable}
|
||||
>
|
||||
@@ -38,16 +47,22 @@
|
||||
<div class="space-y-3">
|
||||
<header class="flex justify-between items-start gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary-500/10 rounded-lg text-primary-500 group-hover:bg-primary-500 group-hover:text-white transition-colors">
|
||||
<div
|
||||
class="p-2 bg-primary-500/10 rounded-lg text-primary-500 group-hover:bg-primary-500 group-hover:text-white transition-colors"
|
||||
>
|
||||
<BookType size="1.5em" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-surface-900 dark:text-surface-100 line-clamp-1">
|
||||
<h3
|
||||
class="text-xl font-bold text-surface-900 dark:text-surface-100 line-clamp-1"
|
||||
>
|
||||
{journal.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
{#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] uppercase tracking-wider font-bold"
|
||||
>
|
||||
{journal.type_code}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -55,26 +70,45 @@
|
||||
|
||||
<!-- Description (Power User / Edit Mode Only) -->
|
||||
{#if journal.description && $ae_loc.edit_mode}
|
||||
<div class="prose prose-sm dark:prose-invert max-h-24 overflow-y-auto bg-surface-100/50 dark:bg-surface-800/50 p-3 rounded-lg text-xs font-mono">
|
||||
<div
|
||||
class="prose prose-sm dark:prose-invert max-h-24 overflow-y-auto bg-surface-100/50 dark:bg-surface-800/50 p-3 rounded-lg text-xs font-mono"
|
||||
>
|
||||
{@html journal.description_md_html}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick Stats (Clean View) -->
|
||||
<div class="flex items-center gap-4 text-xs text-surface-600 dark:text-surface-400">
|
||||
<div class="flex items-center gap-1" title="Entry Count">
|
||||
<div
|
||||
class="flex items-center gap-4 text-xs text-surface-600 dark:text-surface-400"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
title="Entry Count"
|
||||
>
|
||||
<Hash size="1.2em" />
|
||||
<span class="font-bold">{journal.journal_entry_count ?? 0}</span>
|
||||
<span class="font-bold"
|
||||
>{journal.journal_entry_count ?? 0}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-1" title="Last Updated">
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
title="Last Updated"
|
||||
>
|
||||
<Clock size="1.2em" />
|
||||
<span>{ae_util.iso_datetime_formatter(journal.updated_on || journal.created_on, 'date_short')}</span>
|
||||
<span
|
||||
>{ae_util.iso_datetime_formatter(
|
||||
journal.updated_on || journal.created_on,
|
||||
'date_short'
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section: Actions -->
|
||||
<div class="mt-6 pt-4 border-t border-surface-500/10 flex flex-col gap-3">
|
||||
<div
|
||||
class="mt-6 pt-4 border-t border-surface-500/10 flex flex-col gap-3"
|
||||
>
|
||||
<a
|
||||
href="/journals/{journal?.journal_id}"
|
||||
class="btn variant-filled-primary w-full font-bold shadow-md hover:scale-[1.02] active:scale-95 transition-all"
|
||||
@@ -85,9 +119,16 @@
|
||||
|
||||
<!-- Admin Metadata (Edit Mode Only) -->
|
||||
{#if $ae_loc.edit_mode && $ae_loc.administrator_access}
|
||||
<div class="flex items-center justify-center gap-2 text-[10px] opacity-50 font-mono">
|
||||
<div
|
||||
class="flex items-center justify-center gap-2 text-[10px] opacity-50 font-mono"
|
||||
>
|
||||
<Calendar size="1em" />
|
||||
<span>Created: {ae_util.iso_datetime_formatter(journal.created_on, 'datetime_iso_12_no_seconds')}</span>
|
||||
<span
|
||||
>Created: {ae_util.iso_datetime_formatter(
|
||||
journal.created_on,
|
||||
'datetime_iso_12_no_seconds'
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -96,19 +137,27 @@
|
||||
{#if $ae_loc.edit_mode}
|
||||
<div class="absolute -top-2 -right-2 flex gap-1">
|
||||
{#if journal.hide}
|
||||
<span class="badge-icon variant-filled-surface shadow-sm" title="Hidden">🚫</span>
|
||||
<span
|
||||
class="badge-icon variant-filled-surface shadow-sm"
|
||||
title="Hidden">🚫</span
|
||||
>
|
||||
{/if}
|
||||
{#if !journal.enable}
|
||||
<span class="badge-icon variant-filled-warning shadow-sm" title="Disabled">⚠️</span>
|
||||
<span
|
||||
class="badge-icon variant-filled-warning shadow-sm"
|
||||
title="Disabled">⚠️</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="col-span-full p-20 text-center opacity-50 flex flex-col items-center gap-4">
|
||||
<div
|
||||
class="col-span-full p-20 text-center opacity-50 flex flex-col items-center gap-4"
|
||||
>
|
||||
<BookType size="4em" />
|
||||
<p class="text-xl">No journals found in this view.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -22,10 +22,7 @@
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
// *** 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 {
|
||||
journals_loc,
|
||||
journals_sess
|
||||
@@ -37,10 +34,7 @@
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = 0,
|
||||
show = $bindable(false)
|
||||
}: Props = $props();
|
||||
let { log_lvl = 0, show = $bindable(false) }: Props = $props();
|
||||
|
||||
// Internal State
|
||||
let tab: 'form' | 'local_json' | 'session_json' = $state('form');
|
||||
@@ -84,24 +78,32 @@
|
||||
|
||||
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4">
|
||||
<!-- Navigation Tabs -->
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'form' ? 'variant-filled-primary' : 'variant-soft-surface'}"
|
||||
class="btn btn-sm transition-all {tab === 'form'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'form')}
|
||||
>
|
||||
<Settings size="1.1em" class="mr-1" /> Config
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'local_json' ? 'variant-filled-primary' : 'variant-soft-surface'}"
|
||||
class="btn btn-sm transition-all {tab === 'local_json'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'local_json')}
|
||||
>
|
||||
<Database size="1.1em" class="mr-1" /> Local JSON
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'session_json' ? 'variant-filled-primary' : 'variant-soft-surface'}"
|
||||
class="btn btn-sm transition-all {tab === 'session_json'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'session_json')}
|
||||
>
|
||||
<CodeXml size="1.1em" class="mr-1" /> Session JSON
|
||||
@@ -112,29 +114,61 @@
|
||||
<div class="space-y-8 animate-in fade-in duration-300">
|
||||
<!-- Date/Time Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<CalendarClock size="1.2em" class="text-primary-500" />
|
||||
Date and Time Display
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">DateTime Format</span>
|
||||
<select bind:value={tmp_config.datetime_format} class="select select-sm variant-form-material">
|
||||
<option value="datetime_12_short">MMM D, YY hh:mm A</option>
|
||||
<option value="datetime_12_long">MMMM D, YYYY hh:mm A</option>
|
||||
<option value="datetime_short">MMM D, YY HH:mm</option>
|
||||
<option value="datetime_long">MMMM D, YYYY HH:mm</option>
|
||||
<option value="datetime_us">US (MM/DD/YYYY hh:mm:ss A)</option>
|
||||
<option value="datetime_iso">ISO (YYYY-MM-DD HH:mm:ss)</option>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>DateTime Format</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.datetime_format}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="datetime_12_short"
|
||||
>MMM D, YY hh:mm A</option
|
||||
>
|
||||
<option value="datetime_12_long"
|
||||
>MMMM D, YYYY hh:mm A</option
|
||||
>
|
||||
<option value="datetime_short"
|
||||
>MMM D, YY HH:mm</option
|
||||
>
|
||||
<option value="datetime_long"
|
||||
>MMMM D, YYYY HH:mm</option
|
||||
>
|
||||
<option value="datetime_us"
|
||||
>US (MM/DD/YYYY hh:mm:ss A)</option
|
||||
>
|
||||
<option value="datetime_iso"
|
||||
>ISO (YYYY-MM-DD HH:mm:ss)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Time-Only Format</span>
|
||||
<select bind:value={tmp_config.time_format} class="select select-sm variant-form-material">
|
||||
<option value="time_12_short">12-hour short (3:30 PM)</option>
|
||||
<option value="time_12_long">12-hour long (3:30:45 PM)</option>
|
||||
<option value="time_short">24-hour short (15:30)</option>
|
||||
<option value="time_long">24-hour long (15:30:45)</option>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Time-Only Format</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.time_format}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="time_12_short"
|
||||
>12-hour short (3:30 PM)</option
|
||||
>
|
||||
<option value="time_12_long"
|
||||
>12-hour long (3:30:45 PM)</option
|
||||
>
|
||||
<option value="time_short"
|
||||
>24-hour short (15:30)</option
|
||||
>
|
||||
<option value="time_long"
|
||||
>24-hour long (15:30:45)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -142,23 +176,45 @@
|
||||
|
||||
<!-- UI Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<MousePointerClick size="1.2em" class="text-primary-500" />
|
||||
<h2
|
||||
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<MousePointerClick
|
||||
size="1.2em"
|
||||
class="text-primary-500"
|
||||
/>
|
||||
User Interface Preferences
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
|
||||
<label class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp_config.entry.auto_save} class="checkbox" />
|
||||
<label
|
||||
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp_config.entry.auto_save}
|
||||
class="checkbox"
|
||||
/>
|
||||
<div class="space-y-0.5">
|
||||
<span class="font-bold">Enable Auto-Save</span>
|
||||
<p class="text-xs opacity-60">Automatically sync changes while editing</p>
|
||||
<p class="text-xs opacity-60">
|
||||
Automatically sync changes while editing
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10">
|
||||
<input type="checkbox" bind:checked={tmp_config.show_id_random} class="checkbox" />
|
||||
<label
|
||||
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp_config.show_id_random}
|
||||
class="checkbox"
|
||||
/>
|
||||
<div class="space-y-0.5">
|
||||
<span class="font-bold">Show Technical IDs</span>
|
||||
<p class="text-xs opacity-60">Display UUIDs in metadata footers</p>
|
||||
<span class="font-bold">Show Technical IDs</span
|
||||
>
|
||||
<p class="text-xs opacity-60">
|
||||
Display UUIDs in metadata footers
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -166,30 +222,53 @@
|
||||
|
||||
<!-- Journal Query Filters Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Database size="1.2em" class="text-primary-500" />
|
||||
Journal Query Filters
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Enabled Status</span>
|
||||
<select bind:value={tmp_config.journal.qry__enabled} class="select select-sm variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Enabled Status</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.journal.qry__enabled}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="enabled">Enabled Only</option>
|
||||
<option value="not_enabled">Disabled Only</option>
|
||||
<option value="all">All (Enabled & Disabled & NULL)</option>
|
||||
<option value="not_enabled"
|
||||
>Disabled Only</option
|
||||
>
|
||||
<option value="all"
|
||||
>All (Enabled & Disabled & NULL)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Hidden Status</span>
|
||||
<select bind:value={tmp_config.journal.qry__hidden} class="select select-sm variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Hidden Status</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.journal.qry__hidden}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="not_hidden">Visible Only</option>
|
||||
<option value="hidden">Hidden Only</option>
|
||||
<option value="all">All (Visible & Hidden & NULL)</option>
|
||||
<option value="all"
|
||||
>All (Visible & Hidden & NULL)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Query Limit</span>
|
||||
<select bind:value={tmp_config.journal.qry__limit} class="select select-sm variant-form-material">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Query Limit</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.journal.qry__limit}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
@@ -204,30 +283,53 @@
|
||||
|
||||
<!-- Entry Query Filters Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Database size="1.2em" class="text-primary-500" />
|
||||
Entry Query Filters
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Enabled Status</span>
|
||||
<select bind:value={tmp_config.entry.qry__enabled} class="select select-sm variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Enabled Status</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.entry.qry__enabled}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="enabled">Enabled Only</option>
|
||||
<option value="not_enabled">Disabled Only</option>
|
||||
<option value="all">All (Enabled & Disabled & NULL)</option>
|
||||
<option value="not_enabled"
|
||||
>Disabled Only</option
|
||||
>
|
||||
<option value="all"
|
||||
>All (Enabled & Disabled & NULL)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Hidden Status</span>
|
||||
<select bind:value={tmp_config.entry.qry__hidden} class="select select-sm variant-form-material">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Hidden Status</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.entry.qry__hidden}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value="not_hidden">Visible Only</option>
|
||||
<option value="hidden">Hidden Only</option>
|
||||
<option value="all">All (Visible & Hidden & NULL)</option>
|
||||
<option value="all"
|
||||
>All (Visible & Hidden & NULL)</option
|
||||
>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Query Limit</span>
|
||||
<select bind:value={tmp_config.entry.qry__limit} class="select select-sm variant-form-material">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Query Limit</span
|
||||
>
|
||||
<select
|
||||
bind:value={tmp_config.entry.qry__limit}
|
||||
class="select select-sm variant-form-material"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
@@ -242,17 +344,31 @@
|
||||
|
||||
<!-- Security Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<ShieldCheck size="1.2em" class="text-primary-500" />
|
||||
Security & Encryption
|
||||
</h2>
|
||||
<div class="p-4 bg-orange-500/5 rounded-lg border border-orange-500/20 space-y-4">
|
||||
<div
|
||||
class="p-4 bg-orange-500/5 rounded-lg border border-orange-500/20 space-y-4"
|
||||
>
|
||||
<div class="text-sm opacity-80 italic">
|
||||
Global security overrides for the journal module.
|
||||
</div>
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={$journals_sess.enable_session_passcode_cache} class="checkbox checkbox-primary" />
|
||||
<span class="text-sm font-bold">Cache Passcodes in Session</span>
|
||||
<label
|
||||
class="flex items-center space-x-3 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
$journals_sess.enable_session_passcode_cache
|
||||
}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="text-sm font-bold"
|
||||
>Cache Passcodes in Session</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -280,12 +396,20 @@
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex gap-4">
|
||||
<button type="button" class="btn variant-ghost-surface font-bold min-w-[100px]" onclick={() => (show = false)}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-ghost-surface font-bold min-w-[100px]"
|
||||
onclick={() => (show = false)}
|
||||
>
|
||||
<X size="1.2em" class="mr-2" /> Cancel
|
||||
</button>
|
||||
<button type="button" class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]" onclick={handle_save}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]"
|
||||
onclick={handle_save}
|
||||
>
|
||||
<Check size="1.2em" class="mr-2" /> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -16,112 +16,132 @@
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
journal_entry,
|
||||
journal_config,
|
||||
let {
|
||||
open = $bindable(false),
|
||||
journal_entry,
|
||||
journal_config,
|
||||
mode = 'auto',
|
||||
on_close,
|
||||
on_close,
|
||||
on_update,
|
||||
log_lvl = 0
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
// Local State
|
||||
let tmp_entry_obj: key_val = $state({});
|
||||
// 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('');
|
||||
// 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'
|
||||
// Change detection
|
||||
let has_changes: boolean = $derived(
|
||||
add_text_header.length > 0 || add_text.length > 0
|
||||
);
|
||||
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';
|
||||
}
|
||||
// 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 = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Determine Append or Prepend
|
||||
let effective_mode = mode;
|
||||
if (effective_mode === 'auto') {
|
||||
effective_mode = journal_config?.entry_add_text || 'append';
|
||||
}
|
||||
async function handle_save() {
|
||||
let current_entry_content = tmp_entry_obj?.content || '';
|
||||
let add_content = '';
|
||||
let new_content = current_entry_content;
|
||||
|
||||
if (effective_mode == 'prepend') {
|
||||
new_content = add_content + new_content;
|
||||
} else {
|
||||
// Append
|
||||
new_content = new_content.trim() + '\n\n' + add_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') +
|
||||
')'
|
||||
: '';
|
||||
|
||||
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;
|
||||
if (add_timestamp_header && add_text_header) {
|
||||
add_content =
|
||||
'## ' +
|
||||
timestamp_str +
|
||||
day_of_week_str +
|
||||
' - ' +
|
||||
add_text_header.trim() +
|
||||
'\n' +
|
||||
add_text.trim() +
|
||||
'\n\n';
|
||||
} else if (add_timestamp_header) {
|
||||
add_content =
|
||||
'## ' +
|
||||
timestamp_str +
|
||||
day_of_week_str +
|
||||
'\n' +
|
||||
add_text.trim() +
|
||||
'\n\n';
|
||||
} else if (add_text_header) {
|
||||
add_content =
|
||||
'## ' +
|
||||
add_text_header.trim() +
|
||||
day_of_week_str +
|
||||
'\n' +
|
||||
add_text.trim() +
|
||||
'\n\n';
|
||||
} else {
|
||||
add_content = add_text.trim() + '\n\n';
|
||||
}
|
||||
|
||||
// Determine Append or Prepend
|
||||
let effective_mode = mode;
|
||||
if (effective_mode === 'auto') {
|
||||
effective_mode = journal_config?.entry_add_text || 'append';
|
||||
}
|
||||
|
||||
if (effective_mode == 'prepend') {
|
||||
new_content = add_content + new_content;
|
||||
} else {
|
||||
// Append
|
||||
new_content = new_content.trim() + '\n\n' + add_content;
|
||||
}
|
||||
|
||||
new_content = new_content.trim() + '\n';
|
||||
|
||||
let data_kv = { content: new_content };
|
||||
|
||||
try {
|
||||
let update_result =
|
||||
await journals_func.update_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: tmp_entry_obj?.journal_entry_id,
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (update_result) {
|
||||
// Success
|
||||
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.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating journal entry:', error);
|
||||
alert('Failed to update journal entry.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="{(mode === 'auto' ? journal_config?.entry_add_text : mode) == 'append' ? 'Append to' : 'Prepend to'} Journal Entry: {journal_entry?.name ?? journal_entry?.created_on}"
|
||||
bind:open={open}
|
||||
title="{(mode === 'auto' ? journal_config?.entry_add_text : mode) ==
|
||||
'append'
|
||||
? 'Append to'
|
||||
: 'Prepend to'} Journal Entry: {journal_entry?.name ??
|
||||
journal_entry?.created_on}"
|
||||
bind:open
|
||||
autoclose={false}
|
||||
placement="top-center"
|
||||
size="xl"
|
||||
@@ -138,7 +158,10 @@ async function handle_save() {
|
||||
bind:checked={add_timestamp_header}
|
||||
class="p-2 bg-slate-100 text-gray-900 dark:bg-slate-900 dark:text-gray-100 shadow-lg rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-500 dark:hover:border-gray-500 inline-block"
|
||||
/>
|
||||
<label for="append_timestamp_header" class="p-2 inline-block">
|
||||
<label
|
||||
for="append_timestamp_header"
|
||||
class="p-2 inline-block"
|
||||
>
|
||||
Use timestamp as Markdown header
|
||||
</label>
|
||||
|
||||
@@ -148,7 +171,10 @@ async function handle_save() {
|
||||
bind:checked={add_timestamp_header_w_day_of_week}
|
||||
class="p-2 bg-slate-100 text-gray-900 dark:bg-slate-900 dark:text-gray-100 shadow-lg rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-500 dark:hover:border-gray-500 inline-block"
|
||||
/>
|
||||
<label for="append_timestamp_header_w_day_of_week" class="p-2 inline-block">
|
||||
<label
|
||||
for="append_timestamp_header_w_day_of_week"
|
||||
class="p-2 inline-block"
|
||||
>
|
||||
Include day of week
|
||||
</label>
|
||||
</div>
|
||||
@@ -167,12 +193,14 @@ async function handle_save() {
|
||||
class="grow min-h-48 h-full w-full p-2 bg-slate-100 text-gray-900 dark:bg-slate-900 dark:text-gray-100 shadow-lg rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-500 dark:hover:border-gray-500"
|
||||
placeholder="Content to {mode === 'auto'
|
||||
? journal_config?.entry_add_text
|
||||
: mode}...">
|
||||
: mode}..."
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-action flex justify-end gap-2 mt-4">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
disabled={!has_changes}
|
||||
onclick={handle_save}
|
||||
class="btn btn-sm md:btn-md lg:btn-lg min-w-32 hover:variant-outline-success hover:preset-filled-success-500"
|
||||
@@ -182,7 +210,8 @@ async function handle_save() {
|
||||
<Check class="mr-1" />
|
||||
Update
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={on_close}
|
||||
class="btn preset-tonal-surface border border-surface-500 hover:preset-filled-surface-500 transition"
|
||||
>
|
||||
@@ -192,4 +221,4 @@ async function handle_save() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
} 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_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/AE_Comp_Editor_CodeMirror.svelte';
|
||||
import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte';
|
||||
@@ -59,7 +62,8 @@
|
||||
|
||||
let tab: 'actions' | 'meta' | 'security' | 'json' = $state('actions');
|
||||
|
||||
const normalize_date = (val: string | null) => val ? val.slice(0, 16) : null;
|
||||
const normalize_date = (val: string | null) =>
|
||||
val ? val.slice(0, 16) : null;
|
||||
|
||||
async function handle_update_entry() {
|
||||
try {
|
||||
@@ -120,17 +124,43 @@
|
||||
|
||||
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4">
|
||||
<!-- Navigation Tabs -->
|
||||
<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">
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'actions' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'actions')}>
|
||||
<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"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'actions'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'actions')}
|
||||
>
|
||||
<Zap size="1.1em" class="mr-1" /> Quick Actions
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'meta' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'meta')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'meta'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'meta')}
|
||||
>
|
||||
<Shapes size="1.1em" class="mr-1" /> Metadata
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'security' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'security')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'security'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'security')}
|
||||
>
|
||||
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm transition-all {tab === 'json' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'json')}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-all {tab === 'json'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (tab = 'json')}
|
||||
>
|
||||
<CodeXml size="1.1em" class="mr-1" /> JSON
|
||||
</button>
|
||||
</div>
|
||||
@@ -138,27 +168,66 @@
|
||||
{#if tab === 'actions'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button type="button" class="btn variant-soft-secondary w-full" onclick={() => { show = false; on_prepend?.(); }}>
|
||||
<ArrowUpToLine size="1.2em" class="mr-2"/> Prepend Content
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-secondary w-full"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_prepend?.();
|
||||
}}
|
||||
>
|
||||
<ArrowUpToLine size="1.2em" class="mr-2" /> Prepend Content
|
||||
</button>
|
||||
<button type="button" class="btn variant-soft-secondary w-full" onclick={() => { show = false; on_append?.(); }}>
|
||||
<ArrowDownToLine size="1.2em" class="mr-2"/> Append Content
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-secondary w-full"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_append?.();
|
||||
}}
|
||||
>
|
||||
<ArrowDownToLine size="1.2em" class="mr-2" /> Append Content
|
||||
</button>
|
||||
<button type="button" class="btn variant-soft-surface w-full" onclick={() => { show = false; on_show_export?.(); }}>
|
||||
<FileDown size="1.2em" class="mr-2"/> Export Entry
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface w-full"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_show_export?.();
|
||||
}}
|
||||
>
|
||||
<FileDown size="1.2em" class="mr-2" /> Export Entry
|
||||
</button>
|
||||
<button type="button" class="btn variant-soft-surface w-full" onclick={() => { /* Clone logic here */ alert('Clone not yet implemented in modal'); }}>
|
||||
<Copy size="1.2em" class="mr-2"/> Clone Entry
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-surface w-full"
|
||||
onclick={() => {
|
||||
/* Clone logic here */ alert(
|
||||
'Clone not yet implemented in modal'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Copy size="1.2em" class="mr-2" /> Clone Entry
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4 pt-4 border-t border-surface-500/20">
|
||||
<h4 class="text-xs font-bold uppercase opacity-50">Quick Category</h4>
|
||||
<h4 class="text-xs font-bold uppercase opacity-50">
|
||||
Quick Category
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each journal?.cfg_json?.category_li ?? [] as cat}
|
||||
<button type="button"
|
||||
class="btn btn-sm {tmp_entry_obj.category_code === cat.code ? 'variant-filled-primary' : 'variant-soft-primary'}"
|
||||
onclick={() => { tmp_entry_obj.category_code = cat.code; handle_update_entry(); on_save(); }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {tmp_entry_obj.category_code ===
|
||||
cat.code
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-primary'}"
|
||||
onclick={() => {
|
||||
tmp_entry_obj.category_code = cat.code;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
@@ -166,12 +235,18 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'meta'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Category</span>
|
||||
<select class="select variant-form-material" bind:value={tmp_entry_obj.category_code} onchange={() => { handle_update_entry(); on_save(); }}>
|
||||
<select
|
||||
class="select variant-form-material"
|
||||
bind:value={tmp_entry_obj.category_code}
|
||||
onchange={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
>
|
||||
<option value="">None</option>
|
||||
{#each journal?.cfg_json?.category_li ?? [] as cat}
|
||||
<option value={cat.code}>{cat.name}</option>
|
||||
@@ -180,94 +255,223 @@
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Tags (Comma separated)</span>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Tags (Comma separated)</span
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Tag size="1.2em" class="opacity-30" />
|
||||
<input type="text" bind:value={tmp_entry_obj.tags} class="input variant-form-material grow" placeholder="meeting, urgent, ideas" onchange={() => { handle_update_entry(); on_save(); }} />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={tmp_entry_obj.tags}
|
||||
class="input variant-form-material grow"
|
||||
placeholder="meeting, urgent, ideas"
|
||||
onchange={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Archive On</span>
|
||||
<input type="datetime-local" value={normalize_date(tmp_entry_obj.archive_on)} onchange={(e) => { tmp_entry_obj.archive_on = e.currentTarget.value; handle_update_entry(); on_save(); }} class="input variant-form-material" />
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Archive On</span
|
||||
>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={normalize_date(tmp_entry_obj.archive_on)}
|
||||
onchange={(e) => {
|
||||
tmp_entry_obj.archive_on =
|
||||
e.currentTarget.value;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
class="input variant-form-material"
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-sm font-bold opacity-70">Sort Priority</span>
|
||||
<span class="text-sm font-bold opacity-70"
|
||||
>Sort Priority</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp_entry_obj.sort = (tmp_entry_obj.sort ?? 0) - 1; handle_update_entry(); on_save(); }}><Minus size="1em"/></button>
|
||||
<span class="font-mono font-bold w-8 text-center">{tmp_entry_obj.sort ?? 0}</span>
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp_entry_obj.sort = (tmp_entry_obj.sort ?? 0) + 1; handle_update_entry(); on_save(); }}><Plus size="1em"/></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp_entry_obj.sort =
|
||||
(tmp_entry_obj.sort ?? 0) - 1;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}><Minus size="1em" /></button
|
||||
>
|
||||
<span class="font-mono font-bold w-8 text-center"
|
||||
>{tmp_entry_obj.sort ?? 0}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp_entry_obj.sort =
|
||||
(tmp_entry_obj.sort ?? 0) + 1;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}><Plus size="1em" /></button
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'security'}
|
||||
<div class="space-y-6 animate-in fade-in duration-300">
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
|
||||
<h2
|
||||
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2"
|
||||
>
|
||||
<Fingerprint size="1.2em" class="text-primary-500" />
|
||||
Status & Security
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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">
|
||||
<input type="checkbox" bind:checked={tmp_entry_obj.enable} onchange={() => { handle_update_entry(); on_save(); }} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp_entry_obj.enable}
|
||||
onchange={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Enabled</span>
|
||||
<span class="text-xs opacity-60">Allow access to this entry</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Allow access to this entry</span
|
||||
>
|
||||
</div>
|
||||
</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">
|
||||
<input type="checkbox" bind:checked={tmp_entry_obj.hide} onchange={() => { handle_update_entry(); on_save(); }} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp_entry_obj.hide}
|
||||
onchange={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Hidden</span>
|
||||
<span class="text-xs opacity-60">Hide from standard lists</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Hide from standard lists</span
|
||||
>
|
||||
</div>
|
||||
</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">
|
||||
<input type="checkbox" bind:checked={tmp_entry_obj.priority} onchange={() => { handle_update_entry(); on_save(); }} class="checkbox checkbox-primary" />
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={tmp_entry_obj.priority}
|
||||
onchange={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">Priority Entry</span>
|
||||
<span class="text-xs opacity-60">Star or pin to top</span>
|
||||
<span class="text-xs opacity-60"
|
||||
>Star or pin to top</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10">
|
||||
<div
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-sm">Sort Order</span>
|
||||
<span class="text-xs opacity-60">Manual list position</span>
|
||||
<span class="font-bold text-sm">Sort Order</span
|
||||
>
|
||||
<span class="text-xs opacity-60"
|
||||
>Manual list position</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp_entry_obj.sort = (tmp_entry_obj.sort ?? 0) - 1; handle_update_entry(); on_save(); }}><Minus size="1em"/></button>
|
||||
<span class="font-mono font-bold w-8 text-center text-lg">{tmp_entry_obj.sort ?? 0}</span>
|
||||
<button type="button" class="btn-icon btn-icon-sm variant-soft-surface" onclick={() => { tmp_entry_obj.sort = (tmp_entry_obj.sort ?? 0) + 1; handle_update_entry(); on_save(); }}><Plus size="1em"/></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp_entry_obj.sort =
|
||||
(tmp_entry_obj.sort ?? 0) - 1;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}><Minus size="1em" /></button
|
||||
>
|
||||
<span
|
||||
class="font-mono font-bold w-8 text-center text-lg"
|
||||
>{tmp_entry_obj.sort ?? 0}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-surface"
|
||||
onclick={() => {
|
||||
tmp_entry_obj.sort =
|
||||
(tmp_entry_obj.sort ?? 0) + 1;
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}><Plus size="1em" /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<h4 class="text-xs font-bold uppercase opacity-50">Privacy Flags</h4>
|
||||
<h4 class="text-xs font-bold uppercase opacity-50">
|
||||
Privacy Flags
|
||||
</h4>
|
||||
<AE_Object_Flags
|
||||
bind:obj={tmp_entry_obj}
|
||||
on_toggle={() => { handle_update_entry(); on_save(); }}
|
||||
on_toggle={() => {
|
||||
handle_update_entry();
|
||||
on_save();
|
||||
}}
|
||||
hide_alert={journal?.cfg_json?.hide_btn_alert}
|
||||
hide_private={journal?.cfg_json?.hide_btn_private}
|
||||
hide_public={journal?.cfg_json?.hide_btn_public}
|
||||
hide_personal={journal?.cfg_json?.hide_btn_personal}
|
||||
hide_professional={journal?.cfg_json?.hide_btn_professional}
|
||||
hide_professional={journal?.cfg_json
|
||||
?.hide_btn_professional}
|
||||
hide_template={journal?.cfg_json?.hide_btn_template}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{#if tmp_entry_obj.private && !tmp_entry_obj.content && tmp_entry_obj.content_encrypted}
|
||||
<section class="pt-8 border-t border-error-500/20">
|
||||
<div class="bg-error-500/10 p-4 rounded-lg border border-error-500/30">
|
||||
<h4 class="text-error-500 font-bold flex items-center gap-2 mb-2">
|
||||
<div
|
||||
class="bg-error-500/10 p-4 rounded-lg border border-error-500/30"
|
||||
>
|
||||
<h4
|
||||
class="text-error-500 font-bold flex items-center gap-2 mb-2"
|
||||
>
|
||||
<RefreshCcw size="1.2em" /> Disaster Recovery
|
||||
</h4>
|
||||
<p class="text-xs opacity-70 mb-4 italic">If the encryption passcode is lost, the data is unrecoverable. You can force a reset to plain text to reuse this entry ID.</p>
|
||||
<button type="button" class="btn btn-sm variant-filled-error w-full font-bold" onclick={() => { show = false; on_force_reset?.(); }}>
|
||||
<p class="text-xs opacity-70 mb-4 italic">
|
||||
If the encryption passcode is lost, the data is
|
||||
unrecoverable. You can force a reset to plain
|
||||
text to reuse this entry ID.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-error w-full font-bold"
|
||||
onclick={() => {
|
||||
show = false;
|
||||
on_force_reset?.();
|
||||
}}
|
||||
>
|
||||
Force Reset to Plain Text
|
||||
</button>
|
||||
</div>
|
||||
@@ -275,12 +479,17 @@
|
||||
{/if}
|
||||
|
||||
<section class="pt-12">
|
||||
<button type="button" class="btn btn-sm variant-soft-error w-full" onclick={() => { alert('Delete logic handled in parent component'); }}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-error w-full"
|
||||
onclick={() => {
|
||||
alert('Delete logic handled in parent component');
|
||||
}}
|
||||
>
|
||||
<Trash2 size="1.1em" class="mr-2" /> Delete Entry
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{:else if tab === 'json'}
|
||||
<div class="h-full min-h-[400px]">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
@@ -294,9 +503,13 @@
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
<button type="button" class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]" onclick={() => (show = false)}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]"
|
||||
onclick={() => (show = false)}
|
||||
>
|
||||
<Check size="1.2em" class="mr-2" />
|
||||
Done
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -7,10 +7,20 @@
|
||||
*/
|
||||
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import { Download, Copy, FileJson, FileType, Code, Settings2 } from 'lucide-svelte';
|
||||
import {
|
||||
Download,
|
||||
Copy,
|
||||
FileJson,
|
||||
FileType,
|
||||
Code,
|
||||
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';
|
||||
import {
|
||||
EXPORT_TEMPLATES,
|
||||
type ExportTemplate
|
||||
} from '$lib/ae_journals/ae_journals_export_templates';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -19,7 +29,12 @@
|
||||
on_close: () => void;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), entries = [], journal, on_close }: Props = $props();
|
||||
let {
|
||||
open = $bindable(false),
|
||||
entries = [],
|
||||
journal,
|
||||
on_close
|
||||
}: Props = $props();
|
||||
|
||||
// State
|
||||
let selected_template_id: string = $state('standard_markdown');
|
||||
@@ -27,14 +42,24 @@
|
||||
let export_count: number = $derived(entries.length);
|
||||
|
||||
const templates = Object.values(EXPORT_TEMPLATES);
|
||||
const selected_template = $derived(EXPORT_TEMPLATES[selected_template_id as keyof typeof EXPORT_TEMPLATES] || EXPORT_TEMPLATES.standard_markdown);
|
||||
const selected_template = $derived(
|
||||
EXPORT_TEMPLATES[
|
||||
selected_template_id as keyof typeof EXPORT_TEMPLATES
|
||||
] || EXPORT_TEMPLATES.standard_markdown
|
||||
);
|
||||
|
||||
// Auto-select template based on journal type when modal opens
|
||||
$effect(() => {
|
||||
if (open && journal?.type_code) {
|
||||
if (journal.type_code === 'personal_log' && EXPORT_TEMPLATES.personal_log) {
|
||||
if (
|
||||
journal.type_code === 'personal_log' &&
|
||||
EXPORT_TEMPLATES.personal_log
|
||||
) {
|
||||
selected_template_id = 'personal_log';
|
||||
} else if (journal.type_code === 'amazon_vine' && EXPORT_TEMPLATES.amazon_vine) {
|
||||
} else if (
|
||||
journal.type_code === 'amazon_vine' &&
|
||||
EXPORT_TEMPLATES.amazon_vine
|
||||
) {
|
||||
selected_template_id = 'amazon_vine';
|
||||
}
|
||||
}
|
||||
@@ -58,8 +83,10 @@
|
||||
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' });
|
||||
|
||||
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;
|
||||
@@ -86,19 +113,24 @@
|
||||
|
||||
<Modal
|
||||
title="Export Journal Entries"
|
||||
bind:open={open}
|
||||
bind:open
|
||||
autoclose={false}
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<div>
|
||||
<strong>Ready to Export:</strong> {export_count} entries.
|
||||
<strong>Ready to Export:</strong>
|
||||
{export_count} entries.
|
||||
</div>
|
||||
{#if journal}
|
||||
<div class="text-xs opacity-70">
|
||||
Journal Type: <span class="font-mono">{journal.type_code || 'standard'}</span>
|
||||
Journal Type: <span class="font-mono"
|
||||
>{journal.type_code || 'standard'}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -111,10 +143,16 @@
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
|
||||
{#each templates as template}
|
||||
<button type="button"
|
||||
|
||||
class="btn variant-ringed-surface flex flex-col gap-1 p-2 h-24 items-center justify-center border-2 text-center {selected_template_id === template.id ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : 'opacity-60'}"
|
||||
onclick={() => { selected_template_id = template.id; generate_preview(); }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-ringed-surface flex flex-col gap-1 p-2 h-24 items-center justify-center border-2 text-center {selected_template_id ===
|
||||
template.id
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||||
: 'opacity-60'}"
|
||||
onclick={() => {
|
||||
selected_template_id = template.id;
|
||||
generate_preview();
|
||||
}}
|
||||
>
|
||||
{#if template.extension === 'json'}
|
||||
<FileJson size="1.5em" />
|
||||
@@ -123,8 +161,12 @@
|
||||
{:else}
|
||||
<FileType size="1.5em" />
|
||||
{/if}
|
||||
<span class="text-xs font-bold leading-tight">{template.name}</span>
|
||||
<span class="text-[10px] opacity-70">.{template.extension}</span>
|
||||
<span class="text-xs font-bold leading-tight"
|
||||
>{template.name}</span
|
||||
>
|
||||
<span class="text-[10px] opacity-70"
|
||||
>.{template.extension}</span
|
||||
>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -134,27 +176,44 @@
|
||||
<div class="form-control">
|
||||
<div class="label flex justify-between items-center">
|
||||
<span class="label-text">Preview</span>
|
||||
<span class="text-[10px] opacity-50 uppercase tracking-widest">{selected_template.name}</span>
|
||||
<span class="text-[10px] opacity-50 uppercase tracking-widest"
|
||||
>{selected_template.name}</span
|
||||
>
|
||||
</div>
|
||||
<textarea
|
||||
<textarea
|
||||
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900"
|
||||
readonly
|
||||
value={export_preview.substring(0, 5000) + (export_preview.length > 5000 ? '\n... (truncated for preview)' : '')}
|
||||
readonly
|
||||
value={export_preview.substring(0, 5000) +
|
||||
(export_preview.length > 5000
|
||||
? '\n... (truncated for preview)'
|
||||
: '')}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="modal-action flex justify-between items-center">
|
||||
<button type="button" class="btn preset-tonal-secondary" onclick={on_close}>Close</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-secondary"
|
||||
onclick={on_close}>Close</button
|
||||
>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn variant-soft-primary" onclick={handle_copy}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft-primary"
|
||||
onclick={handle_copy}
|
||||
>
|
||||
<Copy class="mr-2" size="1.2em" /> Copy to Clipboard
|
||||
</button>
|
||||
<button type="button" class="btn preset-filled-primary" onclick={handle_download}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-filled-primary"
|
||||
onclick={handle_download}
|
||||
>
|
||||
<Download class="mr-2" size="1.2em" /> Download File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import { Upload, FileText, AlertCircle, Check, X, RefreshCw } from 'lucide-svelte';
|
||||
import { PARSERS, type AeJournalEntryInput } from '$lib/ae_journals/ae_journals_parsers';
|
||||
import {
|
||||
Upload,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw
|
||||
} 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';
|
||||
@@ -12,7 +22,11 @@
|
||||
on_import_complete: () => void;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), on_close, on_import_complete }: Props = $props();
|
||||
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');
|
||||
@@ -30,28 +44,28 @@
|
||||
});
|
||||
|
||||
function handle_drag_enter(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
e.prevent_default();
|
||||
e.stopPropagation();
|
||||
is_dragging = true;
|
||||
}
|
||||
|
||||
function handle_drag_leave(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
e.prevent_default();
|
||||
e.stopPropagation();
|
||||
is_dragging = false;
|
||||
}
|
||||
|
||||
function handle_drag_over(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
e.prevent_default();
|
||||
e.stopPropagation();
|
||||
is_dragging = true;
|
||||
}
|
||||
|
||||
function handle_drop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
e.prevent_default();
|
||||
e.stopPropagation();
|
||||
is_dragging = false;
|
||||
|
||||
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
files = e.dataTransfer.files;
|
||||
}
|
||||
@@ -61,9 +75,9 @@
|
||||
if (!files) return;
|
||||
is_parsing = true;
|
||||
parsed_entries = [];
|
||||
|
||||
|
||||
const parser = PARSERS[selected_parser];
|
||||
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
try {
|
||||
@@ -84,7 +98,9 @@
|
||||
|
||||
const journal_id = $journals_slct.journal_id;
|
||||
if (!journal_id) {
|
||||
alert("No target journal selected. Please select a journal in the background first.");
|
||||
alert(
|
||||
'No target journal selected. Please select a journal in the background first.'
|
||||
);
|
||||
is_importing = false;
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +138,9 @@
|
||||
}
|
||||
|
||||
is_importing = false;
|
||||
alert(`Import complete! ${success_count}/${parsed_entries.length} imported.`);
|
||||
alert(
|
||||
`Import complete! ${success_count}/${parsed_entries.length} imported.`
|
||||
);
|
||||
on_import_complete();
|
||||
open = false;
|
||||
}
|
||||
@@ -130,7 +148,7 @@
|
||||
|
||||
<Modal
|
||||
title="Import Journal Entries"
|
||||
bind:open={open}
|
||||
bind:open
|
||||
autoclose={false}
|
||||
size="xl"
|
||||
class="w-full"
|
||||
@@ -142,8 +160,12 @@
|
||||
<label class="label">
|
||||
<span>Parser Strategy</span>
|
||||
<select class="select" bind:value={selected_parser}>
|
||||
<option value="standard">Standard (1 File = 1 Entry)</option>
|
||||
<option value="personal_log">Personal Log (Split by Date)</option>
|
||||
<option value="standard"
|
||||
>Standard (1 File = 1 Entry)</option
|
||||
>
|
||||
<option value="personal_log"
|
||||
>Personal Log (Split by Date)</option
|
||||
>
|
||||
<option value="amazon_vine">Amazon Vine Reviews</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -152,50 +174,60 @@
|
||||
<div class="label mb-2">
|
||||
<span>Select Files</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Drop Zone -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
<div
|
||||
class="
|
||||
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all duration-200
|
||||
flex flex-col items-center justify-center gap-2
|
||||
{is_dragging ? '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'}
|
||||
{is_dragging
|
||||
? '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'}
|
||||
"
|
||||
ondragenter={handle_drag_enter}
|
||||
ondragleave={handle_drag_leave}
|
||||
ondragover={handle_drag_over}
|
||||
ondrop={handle_drop}
|
||||
onclick={() => document.getElementById('file_import_input')?.click()}
|
||||
onclick={() =>
|
||||
document.getElementById('file_import_input')?.click()}
|
||||
>
|
||||
<Upload class="h-10 w-10 text-gray-400" />
|
||||
<p class="text-sm text-gray-500">
|
||||
<span class="font-semibold text-primary-600 hover:text-primary-500">Click to upload</span>
|
||||
<span
|
||||
class="font-semibold text-primary-600 hover:text-primary-500"
|
||||
>Click to upload</span
|
||||
>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">Markdown (.md) or Text (.txt) files</p>
|
||||
|
||||
<input
|
||||
<p class="text-xs text-gray-400">
|
||||
Markdown (.md) or Text (.txt) files
|
||||
</p>
|
||||
|
||||
<input
|
||||
id="file_import_input"
|
||||
type="file"
|
||||
class="hidden"
|
||||
multiple
|
||||
type="file"
|
||||
class="hidden"
|
||||
multiple
|
||||
accept=".md,.txt"
|
||||
onchange={(e) => files = e.currentTarget.files}
|
||||
onchange={(e) => (files = e.currentTarget.files)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="border rounded-lg p-2 bg-gray-50 dark:bg-gray-900 max-h-64 overflow-y-auto">
|
||||
<div
|
||||
class="border rounded-lg p-2 bg-gray-50 dark:bg-gray-900 max-h-64 overflow-y-auto"
|
||||
>
|
||||
<h4 class="font-bold mb-2 flex justify-between">
|
||||
<span>Preview ({parsed_entries.length} entries)</span>
|
||||
{#if is_parsing}
|
||||
<RefreshCw class="animate-spin" />
|
||||
{/if}
|
||||
</h4>
|
||||
|
||||
|
||||
{#if parsed_entries.length > 0}
|
||||
<table class="table table-compact w-full text-xs">
|
||||
<thead>
|
||||
@@ -208,23 +240,32 @@
|
||||
<tbody>
|
||||
{#each parsed_entries as entry}
|
||||
<tr>
|
||||
<td class="truncate max-w-[200px]" title={entry.name}>{entry.name}</td>
|
||||
<td>{entry.created_on?.substring(0,10)}</td>
|
||||
<td
|
||||
class="truncate max-w-[200px]"
|
||||
title={entry.name}>{entry.name}</td
|
||||
>
|
||||
<td>{entry.created_on?.substring(0, 10)}</td>
|
||||
<td>{entry.tags.join(', ')}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else if files && files.length > 0 && !is_parsing}
|
||||
<div class="text-center text-gray-500 py-4">No entries found in selected files.</div>
|
||||
<div class="text-center text-gray-500 py-4">
|
||||
No entries found in selected files.
|
||||
</div>
|
||||
{:else if !files}
|
||||
<div class="text-center text-gray-500 py-4">Select files to preview import.</div>
|
||||
<div class="text-center text-gray-500 py-4">
|
||||
Select files to preview import.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Import Log -->
|
||||
{#if import_log.length > 0}
|
||||
<div class="bg-black text-green-400 p-2 rounded text-xs font-mono max-h-32 overflow-y-auto">
|
||||
<div
|
||||
class="bg-black text-green-400 p-2 rounded text-xs font-mono max-h-32 overflow-y-auto"
|
||||
>
|
||||
{#each import_log as log}
|
||||
<div>{log}</div>
|
||||
{/each}
|
||||
@@ -232,9 +273,14 @@
|
||||
{/if}
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn preset-tonal-secondary" onclick={on_close}>Cancel</button>
|
||||
<button type="button"
|
||||
class="btn preset-filled-primary"
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-secondary"
|
||||
onclick={on_close}>Cancel</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-filled-primary"
|
||||
disabled={parsed_entries.length === 0 || is_importing}
|
||||
onclick={handle_import}
|
||||
>
|
||||
|
||||
@@ -97,7 +97,9 @@
|
||||
let ae_promises: key_val = $state({});
|
||||
</script>
|
||||
|
||||
<section class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-between w-full">
|
||||
<section
|
||||
class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-between w-full"
|
||||
>
|
||||
<!-- {lq__journal_entry_obj?.priority}
|
||||
{lq__journal_entry_obj?.sort}
|
||||
{lq__journal_entry_obj?.group}
|
||||
@@ -105,7 +107,8 @@
|
||||
{obj_enable} -->
|
||||
|
||||
<!-- Set/unset priority (boolean) -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_priority = !obj_priority;
|
||||
// update_journal_entry();
|
||||
@@ -126,7 +129,8 @@
|
||||
class:hidden={!$ae_loc.edit_mode}
|
||||
class="flex flex-row flex-wrap items-center justify-center border border-gray-300 rounded-lg"
|
||||
>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_sort = obj_sort ? obj_sort + 1 : 1;
|
||||
// update_journal_entry();
|
||||
@@ -144,7 +148,8 @@
|
||||
<ArrowDown10 />
|
||||
{/if}
|
||||
</span>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_sort = obj_sort ? obj_sort - 1 : 0;
|
||||
// update_journal_entry();
|
||||
@@ -187,7 +192,8 @@
|
||||
|
||||
{#if obj_archive_on}
|
||||
<!-- Button to clear the datetime -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_archive_on = null;
|
||||
// update_journal_entry();
|
||||
@@ -201,12 +207,16 @@
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Button to set the datetime to now -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
// tmp_entry_obj.archive_on = new Date().toISOString();
|
||||
// console.log('Archive on datetime set to now:', tmp_entry_obj.archive_on);
|
||||
obj_archive_on = new Date().toISOString();
|
||||
console.log('Archive on datetime set to now:', tmp_entry_obj.archive_on);
|
||||
console.log(
|
||||
'Archive on datetime set to now:',
|
||||
tmp_entry_obj.archive_on
|
||||
);
|
||||
// update_journal_entry();
|
||||
}}
|
||||
class:hidden={!$ae_loc.edit_mode}
|
||||
@@ -220,7 +230,8 @@
|
||||
</span>
|
||||
|
||||
<!-- Set/unset hide (boolean) -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_hide = !obj_hide;
|
||||
update_journal_entry();
|
||||
@@ -238,7 +249,8 @@
|
||||
</button>
|
||||
|
||||
<!-- Set/unset enable (boolean) -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
obj_enable = !obj_enable;
|
||||
// update_journal_entry();
|
||||
@@ -257,9 +269,12 @@
|
||||
</button>
|
||||
|
||||
<!-- Delete journal entry -->
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if (confirm(`Are you sure you want to delete this journal entry?`)) {
|
||||
if (
|
||||
confirm(`Are you sure you want to delete this journal entry?`)
|
||||
) {
|
||||
let delete_method = 'disable';
|
||||
if ($ae_loc.administrator_access && $ae_loc.edit_mode) {
|
||||
delete_method = 'delete';
|
||||
@@ -267,7 +282,8 @@
|
||||
journals_func
|
||||
.delete_ae_obj_id__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: lq__journal_entry_obj?.journal_entry_id,
|
||||
journal_entry_id:
|
||||
lq__journal_entry_obj?.journal_entry_id,
|
||||
method: delete_method as 'delete' | 'disable' | 'hide',
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
@@ -299,7 +315,9 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<span class="flex flex-row items-center justify-center text-sm text-gray-500">
|
||||
<span
|
||||
class="flex flex-row items-center justify-center text-sm text-gray-500"
|
||||
>
|
||||
{#if !$ae_loc.edit_mode}
|
||||
<span class="">
|
||||
{ae_util.iso_datetime_formatter(
|
||||
@@ -329,7 +347,9 @@
|
||||
class="flex flex-row flex-wrap gap-2 items-center justify-start border border-gray-200 rounded-lg"
|
||||
>
|
||||
<SquareLibrary size="1em" class="mx-1" />
|
||||
<span class="text-sm text-gray-500 hidden sm:inline"> Journal: </span>
|
||||
<span class="text-sm text-gray-500 hidden sm:inline">
|
||||
Journal:
|
||||
</span>
|
||||
<select
|
||||
class="novi_btn btn btn-secondary btn-sm
|
||||
preset-tonal-primary
|
||||
@@ -340,9 +360,15 @@
|
||||
"
|
||||
bind:value={tmp_entry_obj.journal_id}
|
||||
onchange={(event) => {
|
||||
tmp_entry_obj.journal_id = (event.target as HTMLInputElement).value;
|
||||
tmp_entry_obj.journal_id = (
|
||||
event.target as HTMLInputElement
|
||||
).value;
|
||||
console.log('Selected journal:', tmp_entry_obj.journal_id);
|
||||
if (confirm(`Are you sure you want to change the journal for this entry?`)) {
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to change the journal for this entry?`
|
||||
)
|
||||
) {
|
||||
change_journal_id();
|
||||
}
|
||||
}}
|
||||
@@ -350,7 +376,10 @@
|
||||
>
|
||||
<option value="">Select Journal</option>
|
||||
{#each lq__journal_obj_li as journal}
|
||||
<option value={journal.journal_id} title={`Journal: ${journal.name}`}>
|
||||
<option
|
||||
value={journal.journal_id}
|
||||
title={`Journal: ${journal.name}`}
|
||||
>
|
||||
{journal.name}
|
||||
</option>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user