ui: a generated form guards its unsaved changes, and Save stays on the document (#7359) - #7361
Open
delchev wants to merge 1 commit into
Open
ui: a generated form guards its unsaved changes, and Save stays on the document (#7359)#7361delchev wants to merge 1 commit into
delchev wants to merge 1 commit into
Conversation
…e document (#7359) A generated Harmonia form had no notion of unsaved changes. An edited header was dropped without a word by Back to list, the footer Cancel, a sidebar entry, the browser's Back button and a reload; nothing marked the form as dirty; saving a line item while the header was dirty committed the line and left the user believing the whole document was saved; and Save in edit mode navigated to the list, so saving and leaving were the same click. The mechanism is one, and it lives in the SHARED runtime: - basePage snapshots the SAVE PAYLOAD after every load and after every successful save (toPayload already normalizes dates, dropdown ids and a multiselect csv, so "3" vs 3 does not read as an edit) and is dirty while the buffer differs from it. Preview and a document the /mutable pre-check reported closed can never be dirty. - App.leaveGuard is what the open form registers with. In-app navigation is vetoed through Pinecone's own global handler - a handler that throws aborts navigate before the route renders and before the history entry is pushed (router 7.5.2) - so the page keeps its address while the dialog is up; the browser's Back button has already moved the URL, which restoreHash puts back; a reload or a closed tab falls to beforeunload. - One dialog per view with three answers: Save and leave (Save and continue when the user is staying), Discard, Keep editing. An "Unsaved changes" badge in the header bar, Save disabled on a clean edit form, and the footer Cancel relabelled to Discard while dirty. Wired into every generated form that owns an editable header: the document page (plus its line-item Add / Fill Month / line delete, each of which would otherwise save behind an unsaved header), the manage form, and the my / partner form and document pages. On the document surfaces Save now STAYS on the record - it re-reads the header and toasts "Saved" - and clears the guard before any navigation it performs itself. The admin inline editor is deliberately left out: it is a standalone page with no router and no shared runtime, and its Cancel is an explicit action inside its own panel. Verified: DependsOnHarmoniaIT (Chrome) drives the journey on a generated form - dirty badge, Back to list vetoed, the dialog, Keep editing, the edit still there; HarmoniaUnsavedChangesIT is the sweep that keeps a surface from being left behind; ModelGenerationIT (every template renders, twice) and IntentEmissionCoverageIT (a generated intent app with a document and a personal surface) both green; formatter:validate green with the cache wiped. Fixes #7359 Co-Authored-By: Claude Opus 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.
Cause
A generated Harmonia form had no notion of unsaved changes.
loadHeader()/loadRecord()copied the record intothis.formand kept no pristine snapshot, so nothing anywhere could tell an edited form from a saved one -grep -ri "dirty\|beforeunload\|unsaved"over the whole template set returned nothing. Every way out dropped the edit silently:backToList()/cancel()navigated directly, a sidebar entry or the browser's Back button went through Pinecone with nobody asking, and a reload or a closed tab took the buffer with it. Two adjacent behaviours made it worse: Save in edit mode navigated to the list (save and leave were the same click, while create mode stayed), and saving a line item while the header was dirty committed the line - and re-read the header - so the user believed the whole document was saved.Reported on a sales invoice; it was a property of every generated form.
Change
One mechanism, in the shared runtime (
application-core), wired into every generated form:basePagesnapshots the save payload after each load and each successful save (markPristine), and is dirty while the buffer differs from it. ComparingtoPayload()rather thanformis deliberate: it already normalizes dates, dropdown ids to strings and a multiselect to its csv, so"3"vs3does not read as an edit. Preview and a document the/mutablepre-check reported closed can never be dirty. It also owns the dialog's state and its three answers.App.leaveGuardis what the open form registers with. In-app navigation is vetoed through Pinecone's own global handler: a handler that throws abortsnavigatebefore the route renders and before the history entry is pushed (router 7.5.2), so the page keeps its address while the dialog is up. The browser's Back button has already moved the URL by then -restoreHashputs it back, query string included - and a reload / closed tab falls tobeforeunload.bypassis what lets the guard's own answer perform the navigation it just refused.Surfaces:
perspective/document(including the line-item Add, Fill Month and line delete, each of which would otherwise save behind an unsaved header),perspective/manage, and themy/andpartner/form and document pages. On the document surfaces Save now stays on the record - it re-reads the header and raises a "Saved" toast - and every navigation the page performs itself clears the guard first, or it would veto its own route change. The plainmanageform keeps its save-then-return-to-list behaviour (itsreturnTo/ FK-dialog flows depend on it); only the dropping of the edit is fixed there.Deferred, and why: the
admin/inline editor. It is a standalone page with no Pinecone router and no shared runtime (it loadsapiError.jsalone and speaksfetchdirectly), and its Cancel is an explicit action inside its own panel - wiring it would mean a third implementation of the mechanism rather than a use of this one.Verification
DependsOnHarmoniaIT(Chrome, real generated app) now drives the journey on the form the user is standing on: the picked value makes the form dirty -> the Unsaved changes badge is there -> Back to list is vetoed and the dialog appears -> Keep editing leaves the edit exactly as it was. A snapshot taken at the wrong moment, a member Alpine cannot resolve, or a dialog Harmonia never opens all render as a page that looks right and still loses the edit, so this half can only be proven in a browser. Green.HarmoniaUnsavedChangesIT(new, HTTP-free) is the sweep that keeps a surface from being left behind or written back to a direct navigation: every page snapshots AND guards its exits, every view renders the dialog and the marker, and the shared runtime carries the veto. Green.ModelGenerationIT(every template renders, twice, with the descriptor asserted) andIntentEmissionCoverageIT(a generated intent app with a document master, line items and a personal surface - it compiles and runs the generated output) both green.mvn -T 1C formatter:validategreen with the formatter cache wiped first.Not run locally: the rest of the UI suite, and the PostgreSQL leg - neither is touched by a template/resource change.
Fixes #7359
🤖 Generated with Claude Code