Panel edit drafts: autosaved pending edits with field-level conflict detection (spec 0051)#2566
Merged
Merged
Conversation
…detection (spec 0051) Adds an edit_drafts layer to lunarphp/panel: in-progress edits autosave to a per-staff draft row capturing a base snapshot per touched field, and commits detect conflicts per field against those snapshots, so staff editing disjoint fields never collide. Conflicting commits return a 409 resolved through a keep-mine/take-theirs/manual-merge dialog with a rebase protocol that re-flags fields that move again mid-resolution. Commits are atomic. Wired into the customer details form; other resources opt in by registering a DraftableResource via Section::draftables(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a standardised DraftActions component (autosave status, Discard, Save) that renders in the Breadcrumbs #actions slot only while there is something to save, Shopify-style. The customer edit page swaps its Docs button, in-form save row, and restored-draft banner for the cluster; a hidden submit keeps Enter-to-save working. Exported on ui.ts and the @lunarphp/panel fallback for add-ons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The note is a confirmation, not a permanent label: it appears when a save lands, clears after 10 seconds, and no longer shows stale on page load for a restored draft. "Saving draft" still shows for the duration of a save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FlashMessage rendered where the page put it, so a scrolled viewport never saw it. A new Toaster component renders a fixed bottom-centre stack (teleported to body) fed by a useToasts() store handling success, error and info tones - success/info auto-dismiss after 6s with pause-on-hover, errors stay until closed. Toaster mounts once per shell (PanelLayout and SettingsShell) and bridges the server flash props into the stack, deduped by flash-object identity so layout swaps never double-toast; the middleware now shares flash.info alongside success/error. Pages drop their inline FlashMessage usage; the forgot-password confirmation stays as a persistent inline note since it must remain readable. FlashMessage is removed from the component set and the add-on surface, replaced by Toaster and useToasts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pristine was a plain object, so re-baselining it after commit never invalidated the cached isDirty computed - Discard/Save stayed visible until the next keystroke. Make pristine reactive; test now reads isDirty before committing so the stale-cache regression is covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The animation remount keyed off the points prop reference, so the full prop refresh after a draft commit replayed it with identical data. Compare by value: only actual data changes (e.g. a range switch) restart the draw-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixture-style tests (pruning, cleanup) get a factory; behavioural tests keep creating drafts through DraftManager so snapshot capture stays exercised. First factory in the panel package, autoloaded from both the monorepo root and the split package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements spec 0051 (included in this PR): panel edit forms autosave in-progress changes to a per-staff
edit_draftsrow, and commits detect conflicts per field against a base snapshot captured when each field first entered the draft. Two staff editing different fields on the same record never collide; same-field collisions return a scoped 409 resolved through a keep-mine / take-theirs / manual-merge dialog. Commits are atomic (a conflict applies nothing), and a rebase protocol re-flags any field that moves again mid-resolution.Wired into the customer details form; any resource opts in by registering a
DraftableResourcevia the newSection::draftables()hook.Backend
edit_draftstable (first panel-owned migration; precedent: stripe/opayo),EditDraftmodel —MassPrunablewith a configurable 7-day TTL (lunar.panel.drafts.ttl_days), pruned daily, plus a factory.DraftManagerservice behind a contract: wholesale-replace merge on autosave with snapshot capture, overlay-merge on commit, normalised JSON comparison, validation before conflict detection, atomic apply through core'sUpdatesCustomeraction.EditDraftControllerbehind three per-resource routes (PATCH/DELETE …/draft,POST …/draft/commit), inheriting sectioncan:middleware; the edit page ships adraftInertia prop.Frontend
useEditDraftcomposable: debounced diff autosave (serialised in-flight requests), commit/discard/resolve, 422/409 handling via a new native-fetchhelper (lib/http.ts) with typed errors — the panel's sanctioned non-Inertia transport.DraftActions: Shopify-style contextual save cluster (status text + Discard + Save) in the sticky breadcrumb bar, shown only while there is something to save; the "Draft saved" note self-clears after 10s.DraftConflictDialog: per-field resolution with manual-merge input for scalar values.Toaster+useToastsreplace the inlineFlashMessagebar: fixed bottom-centre stack handling success/error/info (errors sticky), bridging server flash props automatically.flash.infoadded to the shared middleware props.drafts.phplang group across all 16 locales;ui.ts/@lunarphp/panelexport the new surface for add-ons.Notes for review
DraftableResource/DraftManagercontracts,Section::draftables()(also added toProvidesNavigation— breaking only for direct interface implementors, none exist pre-release), the three endpoint shapes and 409/422 payloads, and theui.tsexports.FlashMessageis removed from the add-on surface, replaced byToaster/useToasts.cascadeOnDeleteonstaff_idonly fires on hard deletes; a soft-deleted staff member's drafts linger until the 7-day prune sweeps them — deliberate.staff_idownership, native fetch over axios,POSTcommit.Test plan
vendor/bin/pest --testsuite panel --parallel— 283 passed (29 new: conflict matrix, rebase re-commit, atomicity, normalisation, ownership, pruning, permissions)npm test— 88 passed (31 new across http/composable/dialog/save-cluster/toaster)npm run type-check&&npm run build(+ example add-on workspace build)vendor/bin/phpstan analyse/vendor/bin/pint/lunar:pr-reviewrun; the should-fix (missing factory) addressed in the final commit🤖 Generated with Claude Code