feat: local-first editor#615
Draft
NagariaHussain wants to merge 15 commits into
Draft
Conversation
Introduce draftWorkspace Pinia store that owns optimistic tree, page draft state, and a per-mutation queue backed by the existing CR RPCs. Sidebar create/rename/delete/reorder, content save, and metadata edits all flow through the store with immediate local feedback; submit/merge are gated on pending/failed mutations. See specs/local_first_editor_migration_step_1.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move "View changes" and "Archive" into a three-dot dropdown so the header only surfaces the primary actions (Merge, Submit for Review). Soften the Draft state banner from blue to gray. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the spinning icon and reuse the frappe-ui Badge for the sync state pill. Less visual noise next to the header actions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add three Playwright specs against the local-first draft workspace: delayed create_cr_page, failed update_cr_page, and delayed reorder. A new mock helper wraps page.route() to inject latency and 500s on specific Frappe methods, and SpaceDetails exposes the draft store on window for deterministic optimistic-action testing. Fix a real bug surfaced while writing these: the sidebar's root_node fallback to space.doc.root_group passes a Frappe document name where the store expects a doc_key, silently no-oping the optimistic insert when the dialog opens before hydration completes. Drop the wrong fallback and let createNode default to rootKey when empty. Cleanup: remove leftover console.log calls from ordering and public-pages specs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .or() locator tripped Playwright strict-mode when both the toolbar "New Page" button and the empty-state "Create First Page" button are visible simultaneously, which is the normal state on an empty space since the local-first migration. Add .first() so either match passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a single whitelisted apply_cr_operations endpoint that takes an ordered list of operations (create_node, update_node, update_content, delete_node, move_node, reorder_children), applies them atomically in the request transaction, resolves client temp_keys to server doc_keys, and returns the canonical affected items plus a new operation_version. Extract the existing per-action CR mutation logic into shared internal helpers (_create_cr_item, _update_cr_item, _delete_cr_item, _move_cr_item, _reorder_cr_children, _serialize_cr_item, _compute_cr_route) so both the legacy create_cr_page / update_cr_page / move_cr_page / reorder_cr_children / delete_cr_page RPCs and the new batch endpoint share the same source of truth, including descendant-cascade delete and ancestor-slug route computation. Add operation_version (Int, default 0) to Wiki Change Request and bump it via _bump_operation_version on every mutating path — batch and legacy alike — so the version is a true monotonic counter for any write to the CR head revision. The batch endpoint rejects stale base_version clients with a structured version_conflict response, and get_cr_tree returns operation_version so the frontend can hydrate base_version on load. See specs/local_first_editor_migration_step_2.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Behind a useBatchOperations flag (default on), the draft workspace
store now flushes create/update/save/delete/reorder through a single
applyBatchOps adapter against apply_cr_operations instead of the
per-action CR RPCs. Reorder packs every queued move and its parent's
full sibling order into one atomic batch so a drag is one user intent,
one round trip. The legacy per-RPC paths remain in place under the
flag for rollout safety.
Two correctness guards:
- Batches serialize behind a per-CR sync tail keyed by CR name, with
the latest known operation_version captured at enqueue time. This
prevents same-client concurrent batches racing on base_version
(false version_conflict) and prevents a mid-flight workspace switch
from rerouting a queued batch to the wrong CR.
- operationVersion hydrates from get_cr_tree on load and from every
successful batch response. version_conflict responses surface on
sync.conflict so future UI can offer a "Reload latest" recovery.
The three local-first e2e specs now intercept apply_cr_operations
(delay + fail) instead of the individual CR RPCs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reload latest no longer clears the dirty signal; Submit stays gated until typed content is saved or reverted, instead of silently shipping a CR that lacks what the editor still shows. - All CR-mutating endpoints (apply_cr_operations + 5 legacy RPCs) load the CR through a shared `_lock_and_load_cr` so the `operation_version` monotonic-counter contract survives concurrent batch+legacy writers during rollout, not just batch+batch. - WikiEditor emits dirty-change(false) on the typed-then-undone edge so Submit/merge unblock without a redundant save. - Banner distinguishes "Unsaved changes" (gray) from "Saving…" (orange) — the latter is reserved for in-flight RPCs. - Consolidate the parallel `dirtyEditors` Set onto `page.dirty` via `markPageDirty` / new `markPageClean`; `markPageDirty` lazy-stubs for WikiDocumentPanel's flow where pagesByKey isn't preloaded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refreshing the tab during the 10s autosave window used to lose whatever the user had typed. Now WikiEditor emits a `local-content` event on a 500ms debounce (and flushes on the dirty→clean edge and on unmount). The draft workspace store writes each emission to an idb-keyval store dedicated to wiki drafts so we don't collide with frappe-ui's own IDB usage, and clears the entry once `_doSaveContent` confirms the content reached the server. On `hydrate`, the store reads every draft persisted for the current CR and reseeds `pagesByKey` with `dirty=true`. The banner surfaces this as "Unsaved changes" and Submit/merge stay gated until the restored content is saved or reverted. tmp_* and orphan keys are ignored — Step 3's scope is editor content only, not lost creates or cross-session pending queues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The durable sync banner is the single source of truth for save state in the draft editing flow; per-action success toasts are redundant on top of it. This was the last lingering success toast in DraftContributionPanel / useTreeDialogs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After clicking through the New Page dialog the user had to click into the canvas before typing. Now WikiEditor accepts an autoFocus prop that forwards to TipTap's `autofocus: 'end'` option, and the draft panel passes true while the route is still on a tmp_* key (i.e. the create hasn't synced yet). For everywhere else the editor stays unobtrusive — clicking in still works as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This reverts commit defdb89.
The 1,404-line draftWorkspace.js mixed tree modelling, editor buffers, IndexedDB hydration, transport, operation queueing, temp-key promotion, conflict handling, and UI-derived sync state. Splits the store into a thin coordinator that composes seven cohesive sub-modules under draftWorkspace/: treeModel, pageBuffers, operationQueue, tempKeyResolver, syncTransport, moveScheduler, saveSerializer. The temp-key rewrite now fans out through resolver.onResolve() listeners instead of the resolver reaching into four different maps directly. Public store surface is preserved exactly; full e2e suite (65/65) passes. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Frontend draft workspace store that makes sidebar edits, reorder, and content saves feel local-first while still persisting through the existing CR RPCs. Spec:
specs/local_first_editor_migration_step_1.md.🤖 Generated with Claude Code