fix(field-editor-v3): layout shift, bindable crash, and optimistic display
- Fix layout shift on edit_mode toggle: always render the edit button (using invisible/pointer-events-none) so the flex container doesn't reflow when edit_mode is toggled on/off. - Fix 'store.set is not a function' crash: remove $bindable() from current_value. The component is SWR-first; after a successful PATCH liveQuery updates the prop from Dexie. Trying to write back to a readonly liveQuery-derived prop caused the crash. - Fix stale display after save: add has_optimistic flag + display_value derived. After a successful PATCH, display_value shows draft_value immediately without waiting for liveQuery. Cleared automatically when current_value catches up, or on cancel/re-open of edit mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
object_type,
|
||||
object_id,
|
||||
field_name,
|
||||
current_value = $bindable(),
|
||||
current_value,
|
||||
field_type = 'text',
|
||||
allow_null = false,
|
||||
select_options = {},
|
||||
@@ -70,15 +70,31 @@
|
||||
let error_message = $state('');
|
||||
let draft_value = $state(current_value);
|
||||
|
||||
// Sync draft with current_value when not editing
|
||||
// WHY: Optimistic display. After a successful PATCH, liveQuery may not fire
|
||||
// immediately (or at all if on_success doesn't trigger a Dexie refresh). We show
|
||||
// draft_value as the display until current_value catches up from liveQuery.
|
||||
let has_optimistic = $state(false);
|
||||
let display_value = $derived(has_optimistic ? draft_value : current_value);
|
||||
|
||||
// Sync draft with display_value when not editing.
|
||||
// Suppress reset if optimistic is active — we already have the right value.
|
||||
$effect(() => {
|
||||
if (!is_editing) {
|
||||
untrack(() => {
|
||||
draft_value = current_value;
|
||||
if (!has_optimistic) {
|
||||
draft_value = current_value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clear optimistic once liveQuery catches up (current_value matches what we saved)
|
||||
$effect(() => {
|
||||
if (has_optimistic && current_value === draft_value) {
|
||||
has_optimistic = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function handle_patch() {
|
||||
if (log_lvl) console.log(`AE Field Editor V3: Patching ${object_type}.${field_name}...`);
|
||||
|
||||
@@ -98,7 +114,7 @@
|
||||
|
||||
if (result) {
|
||||
patch_status = 'success';
|
||||
current_value = draft_value;
|
||||
has_optimistic = true; // show draft_value immediately; cleared when liveQuery catches up
|
||||
if (on_success) on_success(result);
|
||||
|
||||
// Close edit mode after a brief success indicator
|
||||
@@ -120,6 +136,7 @@
|
||||
}
|
||||
|
||||
function cancel_edit() {
|
||||
has_optimistic = false;
|
||||
draft_value = current_value;
|
||||
is_editing = false;
|
||||
patch_status = 'idle';
|
||||
@@ -128,7 +145,11 @@
|
||||
|
||||
function toggle_edit() {
|
||||
if (is_editing) cancel_edit();
|
||||
else is_editing = true;
|
||||
else {
|
||||
has_optimistic = false; // clear optimistic so draft syncs from current prop
|
||||
draft_value = current_value;
|
||||
is_editing = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -143,31 +164,33 @@
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if field_type === 'checkbox'}
|
||||
<span class="badge {current_value ? 'variant-filled-success' : 'variant-soft-surface'}">
|
||||
{current_value ? 'Enabled' : 'Disabled'}
|
||||
<span class="badge {display_value ? 'variant-filled-success' : 'variant-soft-surface'}">
|
||||
{display_value ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
{:else if field_type === 'tiptap'}
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
{@html current_value || '<span class="opacity-50 italic">Empty</span>'}
|
||||
{@html display_value || '<span class="opacity-50 italic">Empty</span>'}
|
||||
</div>
|
||||
{:else}
|
||||
<span class:opacity-50={!current_value}>
|
||||
{current_value || 'Not set'}
|
||||
<span class:opacity-50={!display_value}>
|
||||
{display_value || 'Not set'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Edit Trigger (Visible on Hover or Edit Mode) -->
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-warning opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onclick={toggle_edit}
|
||||
title="Edit {field_name}"
|
||||
>
|
||||
<SquarePen size="14" />
|
||||
</button>
|
||||
{/if}
|
||||
<!-- WHY: Always render to avoid layout shift when edit_mode toggles.
|
||||
Use invisible (visibility:hidden, preserves space) when edit_mode is off. -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm variant-soft-warning opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
class:invisible={!$ae_loc.edit_mode}
|
||||
class:pointer-events-none={!$ae_loc.edit_mode}
|
||||
onclick={toggle_edit}
|
||||
title="Edit {field_name}"
|
||||
>
|
||||
<SquarePen size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MODE -->
|
||||
@@ -263,7 +286,7 @@
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-primary"
|
||||
onclick={handle_patch}
|
||||
disabled={patch_status === 'processing' || draft_value === current_value}
|
||||
disabled={patch_status === 'processing' || draft_value === display_value}
|
||||
>
|
||||
{#if patch_status === 'processing'}
|
||||
<LoaderCircle size="14" class="animate-spin mr-1" />
|
||||
|
||||
Reference in New Issue
Block a user