From e8a49562a9efad86a0c24715c4a2025927f3f58d Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 10 Jun 2026 15:15:16 -0400 Subject: [PATCH] feat(pres_mgmt): prefix/suffix inputs, flex row layout, strip :443 from links File Downloads report: add Prefix and Suffix inputs for filename customization (e.g. "2026_06__"), longest-filename length indicator with warning above 120 chars, and switch file rows from table to flex layout so Download/Copy Link buttons stay right-aligned regardless of filename length. Strip redundant :443 from https download URLs in both the file downloads report and the manage event file list clipboard links. Co-Authored-By: Claude Sonnet 4.6 --- .../element_manage_event_file_li.svelte | 7 +- .../reports/reports_file_downloads.svelte | 328 +++++++++++------- 2 files changed, 205 insertions(+), 130 deletions(-) diff --git a/src/lib/elements/element_manage_event_file_li.svelte b/src/lib/elements/element_manage_event_file_li.svelte index a0f4f8fc..9a644dc6 100644 --- a/src/lib/elements/element_manage_event_file_li.svelte +++ b/src/lib/elements/element_manage_event_file_li.svelte @@ -78,6 +78,9 @@ let ae_promises: key_val = $state({}); let ae_tmp: key_val = $state({}); ae_tmp.show__file_li = true; ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download; + +// Strip :443 from https URLs — redundant and clutters shareable links. +let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2')); // let ae_triggers: key_val = {}; onMount(() => { @@ -317,7 +320,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) { = { - original: { label: 'Original filename', group: 'Universal' }, + original: { label: 'Original filename', group: 'Universal' }, session_code: { label: 'Session Code', group: 'Session' }, session_code_name: { label: 'Session Code + Name', group: 'Session' }, session_code_date: { label: 'Session Code + Date', group: 'Session' }, session_code_date_time: { label: 'Session Code + Date + Time', group: 'Session' }, session_code_date_name: { label: 'Session Code + Date + Name', group: 'Session' }, session_presenter: { label: 'Session Code + Presenter (Family, Given)', group: 'Presenter' }, - session_date_presenter: { label: 'Session Code + Date + Presenter', group: 'Presenter' }, - session_presentation_presenter: { label: 'Session Code + Presentation + Presenter', group: 'Presenter' }, + session_date_presenter: { label: 'Session Code + Date + Presenter', group: 'Presenter' }, + session_presentation_presenter: { label: 'Session Code + Presentation + Presenter', group: 'Presenter' }, session_date_presentation_presenter: { label: 'Session Code + Date + Presentation + Presenter (Full)', group: 'Presenter' } }; -// Separate group labels for the elements const FORMAT_GROUPS: Record = { Universal: ['original'], Session: ['session_code', 'session_code_name', 'session_code_date', 'session_code_date_time', 'session_code_date_name'], @@ -92,43 +101,73 @@ const FORMAT_GROUPS: Record = { let selected_format = $state('session_code_name'); +const FILENAME_WARN_LEN = 120; // show warning above this char count + function build_filename(file: any, fmt: FormatKey): string { - const ext = file.extension ?? 'file'; - const session_code = clean_part(file.event_session_code); - const session_name = clean_part(file.event_session_name, 30); - const session_date = date_part(file.event_session_start_datetime); - const session_time = time_part(file.event_session_start_datetime); - const pres_name = clean_part(file.event_presentation_name ?? file.event_session_name, 25); - const fam = clean_part(file.event_presenter_family_name); - const given = clean_part(file.event_presenter_given_name); + const ext = file.extension ?? 'file'; + const pre = clean_affix(prefix_val); + const suf = clean_affix(suffix_val); - if (fmt === 'original') return file.filename ?? `file.${ext}`; + let stem: string; + if (fmt === 'original') { + // Preserve the original stem as-is; only the prefix/suffix are user-controlled. + stem = file.filename_no_ext ?? (file.filename ?? 'file').replace(/\.[^.]+$/, ''); + } else { + const session_code = clean_part(file.event_session_code); + const session_name = clean_part(file.event_session_name, 30); + const session_date = date_part(file.event_session_start_datetime); + const session_time = time_part(file.event_session_start_datetime); + const pres_name = clean_part(file.event_presentation_name ?? file.event_session_name, 25); + const fam = clean_part(file.event_presenter_family_name); + const given = clean_part(file.event_presenter_given_name); - let parts: string[]; - switch (fmt) { - case 'session_code': parts = [session_code]; break; - case 'session_code_name': parts = [session_code, session_name]; break; - case 'session_code_date': parts = [session_code, session_date]; break; - case 'session_code_date_time': parts = [session_code, session_date, session_time]; break; - case 'session_code_date_name': parts = [session_code, session_date, session_name]; break; - case 'session_presenter': parts = [session_code, fam, given]; break; - case 'session_date_presenter': parts = [session_code, session_date, fam, given]; break; - case 'session_presentation_presenter': parts = [session_code, pres_name, fam, given]; break; - case 'session_date_presentation_presenter': parts = [session_code, session_date, pres_name, fam, given]; break; - default: return file.filename ?? `file.${ext}`; + let parts: string[]; + switch (fmt) { + case 'session_code': parts = [session_code]; break; + case 'session_code_name': parts = [session_code, session_name]; break; + case 'session_code_date': parts = [session_code, session_date]; break; + case 'session_code_date_time': parts = [session_code, session_date, session_time]; break; + case 'session_code_date_name': parts = [session_code, session_date, session_name]; break; + case 'session_presenter': parts = [session_code, fam, given]; break; + case 'session_date_presenter': parts = [session_code, session_date, fam, given]; break; + case 'session_presentation_presenter': parts = [session_code, pres_name, fam, given]; break; + case 'session_date_presentation_presenter': parts = [session_code, session_date, pres_name, fam, given]; break; + default: stem = file.filename_no_ext ?? 'file'; return `${pre}${stem}${suf}.${ext}`; + } + stem = parts.filter(Boolean).join('_').replace(/_+/g, '_').replace(/^_|_$/, ''); } - const stem = parts.filter(Boolean).join('_').replace(/_+/g, '_').replace(/^_|_$/, ''); - return `${stem}.${ext}`; + return `${pre}${stem}${suf}.${ext}`; } +// Strip :443 from https URLs — redundant and clutters shareable links. +let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2')); + function build_download_url(file: any, fmt: FormatKey): string { const fname = build_filename(file, fmt); return encodeURI( - `${$ae_api.base_url}/v3/action/event_file/${file.event_file_id}/download?filename=${ae_util.clean_filename(fname)}&key=${$ae_api.account_id}` + `${base_url}/v3/action/event_file/${file.event_file_id}/download?filename=${ae_util.clean_filename(fname)}&key=${$ae_api.account_id}` ); } +// Longest computed filename across all files — reactive to format + prefix + suffix changes. +let max_computed_len = $derived.by(() => { + let max = 0; + for (const sg of session_groups) { + for (const f of sg.session_files) { + const len = build_filename(f, selected_format).length; + if (len > max) max = len; + } + for (const pg of sg.presenter_groups) { + for (const f of pg.files) { + const len = build_filename(f, selected_format).length; + if (len > max) max = len; + } + } + } + return max; +}); + // --------------------------------------------------------------------------- // Grouping // --------------------------------------------------------------------------- @@ -147,8 +186,8 @@ type SessionGroup = { session_code: string; session_name: string; session_start_datetime: string | null; - session_files: any[]; // for_type = 'event_session' - presenter_groups: PresenterGroup[]; // for_type = 'event_presenter' + session_files: any[]; + presenter_groups: PresenterGroup[]; _presenter_map: Record; }; @@ -199,8 +238,8 @@ let session_groups = $derived.by((): SessionGroup[] => { }); // Sort presenter groups within each session client-side (family name, then given name). - // This can't be pushed to the API order_by_li — those are view-only joined columns - // that trigger a backend "account_id" WHERE ambiguity when used in ORDER BY. + // Presenter-level columns are view-only joined fields — per API guide §3B they are + // silently dropped from ORDER BY, so sorting must be done client-side. for (const sg of sorted) { sg.presenter_groups.sort((a, b) => { const fam = a.presenter_family_name.localeCompare(b.presenter_family_name); @@ -212,8 +251,8 @@ let session_groups = $derived.by((): SessionGroup[] => { return sorted; }); -let total_session_files = $derived(session_groups.reduce((n, sg) => n + sg.session_files.length, 0)); -let total_presenter_files = $derived(session_groups.reduce((n, sg) => sg.presenter_groups.reduce((m, pg) => m + pg.files.length, 0) + n, 0)); +let total_session_files = $derived(session_groups.reduce((n, sg) => n + sg.session_files.length, 0)); +let total_presenter_files = $derived(session_groups.reduce((n, sg) => sg.presenter_groups.reduce((m, pg) => m + pg.files.length, 0) + n, 0)); // --------------------------------------------------------------------------- // Query @@ -301,7 +340,7 @@ async function handle_qry() {
- - Applied to all download buttons below. "Copy Link" copies the same URL. - +
+ + +
+ + + {#if qry__count > 0} + FILENAME_WARN_LEN} + class:opacity-50={max_computed_len <= FILENAME_WARN_LEN} + title="Longest computed filename across all files"> + {#if max_computed_len > FILENAME_WARN_LEN} + + {/if} + longest: {max_computed_len} chars + + {/if}
@@ -370,49 +438,51 @@ async function handle_qry() { Session Files ({sg.session_files.length}) - - - {#each sg.session_files as file (file.event_file_id)} - {@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)} - {@const computed_name = build_filename(file, selected_format)} - {@const dl_url = build_download_url(file, selected_format)} - - - - - - - {/each} - -
- - - {ae_util.shorten_filename({ filename: file.filename, max_length: 30 })} - - - - {computed_name} - - - {ae_util.format_bytes(file.file_size)} - - -
+
+ {#each sg.session_files as file (file.event_file_id)} + {@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)} + {@const computed_name = build_filename(file, selected_format)} + {@const dl_url = build_download_url(file, selected_format)} + {@const is_long = computed_name.length > FILENAME_WARN_LEN} +
+ +
+ + + {ae_util.shorten_filename({ filename: file.filename, max_length: 40 })} + + + {computed_name} + +
+ + + {ae_util.format_bytes(file.file_size)} + + + +
+ {/each} +
{/if} @@ -427,49 +497,51 @@ async function handle_qry() { {/if} ({pg.files.length} file{pg.files.length !== 1 ? 's' : ''}) - - - {#each pg.files as file (file.event_file_id)} - {@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)} - {@const computed_name = build_filename(file, selected_format)} - {@const dl_url = build_download_url(file, selected_format)} - - - - - - - {/each} - -
- - - {ae_util.shorten_filename({ filename: file.filename, max_length: 30 })} - - - - {computed_name} - - - {ae_util.format_bytes(file.file_size)} - - -
+
+ {#each pg.files as file (file.event_file_id)} + {@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)} + {@const computed_name = build_filename(file, selected_format)} + {@const dl_url = build_download_url(file, selected_format)} + {@const is_long = computed_name.length > FILENAME_WARN_LEN} +
+ +
+ + + {ae_util.shorten_filename({ filename: file.filename, max_length: 40 })} + + + {computed_name} + +
+ + + {ae_util.format_bytes(file.file_size)} + + + +
+ {/each} +
{/each}