Badges: hide Save until field is dirty; warning style when unsaved changes

This commit is contained in:
Scott Idem
2026-03-12 15:48:26 -04:00
parent f74fc593b0
commit 23422c8f27

View File

@@ -311,6 +311,33 @@
});
});
// --- Dirty detection: compare live edit value against the last-saved badge obj ---
// Save button is hidden when the value matches saved state (no unsaved change);
// shown with a warning/attention style the moment something changes.
// Using string comparison for text fields; boolean comparison for allow_tracking;
// null-safe equality for badge_type_code (null vs null must be equal).
let is_dirty_name = $derived(
edit_full_name_override !== ($lq__event_badge_obj?.full_name_override ?? $lq__event_badge_obj?.full_name ?? '')
);
let is_dirty_title = $derived(
edit_professional_title_override !== ($lq__event_badge_obj?.professional_title_override ?? $lq__event_badge_obj?.professional_title ?? '')
);
let is_dirty_affiliations = $derived(
edit_affiliations_override !== ($lq__event_badge_obj?.affiliations_override ?? $lq__event_badge_obj?.affiliations ?? '')
);
let is_dirty_location = $derived(
edit_location_override !== ($lq__event_badge_obj?.location_override ?? $lq__event_badge_obj?.location ?? '')
);
let is_dirty_pronouns = $derived(
edit_pronouns_override !== ($lq__event_badge_obj?.pronouns_override ?? $lq__event_badge_obj?.pronouns ?? '')
);
let is_dirty_allow_tracking = $derived(
edit_allow_tracking !== ($lq__event_badge_obj?.allow_tracking ?? false)
);
let is_dirty_badge_type = $derived(
edit_badge_type_code !== ($lq__event_badge_obj?.badge_type_code_override ?? $lq__event_badge_obj?.badge_type_code ?? null)
);
// TC modal ref for the lead scanning terms & conditions dialog
let tc_dialog_ref: HTMLDialogElement | undefined;
@@ -371,41 +398,47 @@
{/snippet}
<!-- Save/cancel row (inside accordion edit forms).
on_revert is optional — only passed when an override is currently saved;
clears the override back to the base imported value. Layout: [Revert] [Save] [X] -->
{#snippet field_actions(field_key: string, on_save: () => void, on_cancel: () => void, on_revert?: () => void)}
on_revert is optional — shown only when a saved override exists.
is_dirty: Save is hidden when clean (unchanged), warning-styled when dirty.
Layout when dirty: [Revert?] [⚠ Save] [X] / when clean: [Revert?] [X] -->
{#snippet field_actions(field_key: string, on_save: () => void, on_cancel: () => void, on_revert?: () => void, is_dirty = false)}
{@const status = field_save_status[field_key]}
{@const show_save = is_dirty || (status && status !== 'idle')}
<div class="flex gap-2 mt-2">
{#if on_revert}
<button
type="button"
class="btn btn-sm preset-tonal-warning shrink-0"
onclick={on_revert}
disabled={field_save_status[field_key] === 'saving'}
disabled={status === 'saving'}
title="Remove override — restore original imported value"
aria-label="Revert to original value"
><RotateCcw size="13" /></button>
{/if}
<button
type="button"
class="btn btn-sm flex-1"
class:preset-filled-primary={!field_save_status[field_key] || field_save_status[field_key] === 'idle'}
class:preset-filled-success={field_save_status[field_key] === 'done'}
class:preset-tonal-error={field_save_status[field_key] === 'error'}
disabled={field_save_status[field_key] === 'saving'}
onclick={on_save}
title="Save changes"
aria-label="Save changes"
>
{#if field_save_status[field_key] === 'saving'}
<LoaderCircle size="14" class="animate-spin mr-1" /> Saving…
{:else if field_save_status[field_key] === 'done'}
<Check size="14" class="mr-1" /> Saved
{:else if field_save_status[field_key] === 'error'}
Error — retry
{:else}
<Check size="14" class="mr-1" /> Save
{/if}
</button>
{#if show_save}
<button
type="button"
class="btn btn-sm flex-1 transition-colors"
class:preset-filled-warning={is_dirty && (!status || status === 'idle')}
class:preset-tonal-surface={status === 'saving'}
class:preset-filled-success={status === 'done'}
class:preset-tonal-error={status === 'error'}
disabled={status === 'saving'}
onclick={on_save}
title="Save changes"
aria-label="Save changes"
>
{#if status === 'saving'}
<LoaderCircle size="14" class="animate-spin mr-1" /> Saving…
{:else if status === 'done'}
<Check size="14" class="mr-1" /> Saved
{:else if status === 'error'}
Error — retry
{:else}
<Check size="14" class="mr-1" /> Save
{/if}
</button>
{/if}
<button
type="button"
class="btn btn-sm preset-tonal-surface"
@@ -513,7 +546,8 @@
() => cancel_field('name'),
$lq__event_badge_obj?.full_name_override
? () => save_field('name', { full_name_override: null })
: undefined
: undefined,
is_dirty_name
)}
</div>
</div>
@@ -565,7 +599,8 @@
() => cancel_field('title'),
$lq__event_badge_obj?.professional_title_override
? () => save_field('title', { professional_title_override: null })
: undefined
: undefined,
is_dirty_title
)}
</div>
</div>
@@ -617,7 +652,8 @@
() => cancel_field('affiliations'),
$lq__event_badge_obj?.affiliations_override
? () => save_field('affiliations', { affiliations_override: null })
: undefined
: undefined,
is_dirty_affiliations
)}
</div>
</div>
@@ -669,7 +705,8 @@
() => cancel_field('location'),
$lq__event_badge_obj?.location_override
? () => save_field('location', { location_override: null })
: undefined
: undefined,
is_dirty_location
)}
</div>
</div>
@@ -711,29 +748,33 @@
/>
<span class="text-xs">Allow exhibitor lead scanning</span>
</label>
<!-- Inline actions — not shared snippet because this field adds a TC info button -->
<!-- Inline actions — not shared snippet because this field adds a TC info button.
Save hidden until the checkbox value differs from the saved value. -->
<div class="flex gap-2 mt-2">
<button
type="button"
class="btn btn-sm flex-1"
class:preset-filled-primary={!field_save_status['allow_tracking'] || field_save_status['allow_tracking'] === 'idle'}
class:preset-filled-success={field_save_status['allow_tracking'] === 'done'}
class:preset-tonal-error={field_save_status['allow_tracking'] === 'error'}
disabled={field_save_status['allow_tracking'] === 'saving'}
onclick={() => save_field('allow_tracking', { allow_tracking: edit_allow_tracking })}
title="Save changes"
aria-label="Save changes"
>
{#if field_save_status['allow_tracking'] === 'saving'}
<LoaderCircle size="14" class="animate-spin mr-1" /> Saving…
{:else if field_save_status['allow_tracking'] === 'done'}
<Check size="14" class="mr-1" /> Saved
{:else if field_save_status['allow_tracking'] === 'error'}
Error — retry
{:else}
<Check size="14" class="mr-1" /> Save
{/if}
</button>
{#if is_dirty_allow_tracking || (field_save_status['allow_tracking'] && field_save_status['allow_tracking'] !== 'idle')}
<button
type="button"
class="btn btn-sm flex-1 transition-colors"
class:preset-filled-warning={is_dirty_allow_tracking && (!field_save_status['allow_tracking'] || field_save_status['allow_tracking'] === 'idle')}
class:preset-tonal-surface={field_save_status['allow_tracking'] === 'saving'}
class:preset-filled-success={field_save_status['allow_tracking'] === 'done'}
class:preset-tonal-error={field_save_status['allow_tracking'] === 'error'}
disabled={field_save_status['allow_tracking'] === 'saving'}
onclick={() => save_field('allow_tracking', { allow_tracking: edit_allow_tracking })}
title="Save changes"
aria-label="Save changes"
>
{#if field_save_status['allow_tracking'] === 'saving'}
<LoaderCircle size="14" class="animate-spin mr-1" /> Saving…
{:else if field_save_status['allow_tracking'] === 'done'}
<Check size="14" class="mr-1" /> Saved
{:else if field_save_status['allow_tracking'] === 'error'}
Error — retry
{:else}
<Check size="14" class="mr-1" /> Save
{/if}
</button>
{/if}
<button
type="button"
class="btn btn-sm preset-tonal-surface"
@@ -795,7 +836,8 @@
() => cancel_field('pronouns'),
$lq__event_badge_obj?.pronouns_override
? () => save_field('pronouns', { pronouns_override: null })
: undefined
: undefined,
is_dirty_pronouns
)}
</div>
</div>
@@ -864,7 +906,8 @@
() => cancel_field('badge_type'),
$lq__event_badge_obj?.badge_type_code_override
? () => save_field('badge_type', { badge_type_code_override: null, badge_type_override: null })
: undefined
: undefined,
is_dirty_badge_type
)}
</div>
</div>