Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ document.addEventListener('alpine:init', () => {
...basePage(),
state: 'loading',
mode: 'create',
// True when this session began as a NEW document (create route); stays true after the header is
// saved and `mode` flips to 'edit', so the create flow shows Finish and an existing document opened
// for editing shows Save (issue #7403).
createFlow: false,
id: null,
error: null,
// The single header property a rejected save NAMED ("The 'Name' property is required"), else
Expand Down Expand Up @@ -114,6 +118,7 @@ document.addEventListener('alpine:init', () => {
const params = (window.PineconeRouter.context && window.PineconeRouter.context.params) || {};
this.id = params.id || null;
this.mode = this.id ? 'edit' : 'create';
this.createFlow = this.mode === 'create';
if (this.id) {
try {
const record = await App.services.api.get(this.apiPath + '/' + this.id);
Expand Down Expand Up @@ -638,6 +643,17 @@ document.addEventListener('alpine:init', () => {
// A document with unsaved header edits asks before it is abandoned (issue #7359); a clean one
// leaves silently, as before.
goBack() { this.guardExit(() => this.navigate('/my/${name}')); },

// "Finish" on a saved document (issue #7403). Line items persist as they are added, so the only
// thing still unsaved is a header edit: save it, and only leave if the save succeeded (a failure
// sets this.error and keeps the user on the page). A clean header just returns to the list.
async finish() {
if (this.isDirty()) {
await this.save();
if (this.error) return;
}
this.goBack();
},
#set($docChildren = [])
#if($myChildren)
#foreach($child in $myChildren)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,28 @@
<button x-h-button data-variant="negative" x-show="mode === 'edit'#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="deleteOpen = true"
x-text="T('$projectName:${tprefix}.defaults.delete', 'Delete')"></button>
<div class="flex-1"></div>
<!-- The button says what it will do: leaving a dirty form discards the edit (issue #7359). -->
<!-- Always an escape hatch, kept visible next to Create/Finish as in every other view: Cancel a
clean document, or Discard unsaved header edits (the label flips on isDirty(), issue #7359). -->
<button x-h-button data-variant="transparent" @click="goBack()" :disabled="saving"
x-text="isDirty() ? T('$projectName:${tprefix}.defaults.discard', 'Discard') : T('$projectName:${tprefix}.defaults.cancel', 'Cancel')"></button>
<button x-h-button data-variant="primary"#if($immutableStatusProperty || $immutableAlways || $periodLock) x-show="mutable"#end @click="save()" :disabled="saving || (mode === 'edit' &amp;&amp; !isDirty())">
<!-- Create a new header, then stay to add line items. -->
<button x-h-button data-variant="primary" x-show="mode !== 'edit'#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="save()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.save', 'Save')"></span>
<span x-text="T('$projectName:${tprefix}.defaults.create', 'Create')"></span>
</button>
<!-- Finish: ONLY while creating a new document (adding its line items after the header is saved).
Saves any header change and returns to the list - never a greyed Save on the new document
(issue #7403). -->
<button x-h-button data-variant="primary" x-show="mode === 'edit' &amp;&amp; createFlow#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="finish()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.finish', 'Finish')"></span>
</button>
<!-- Save: editing an EXISTING document saves any header change and returns to the list (issue
#7403); line items already persist as they are added. Reuses finish() (save-if-changed,
then back), so a failed save keeps the user on the page. -->
<button x-h-button data-variant="primary" x-show="mode === 'edit' &amp;&amp; !createFlow#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="finish()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.update', 'Save')"></span>
</button>
#end
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ document.addEventListener('alpine:init', () => {
...basePage(),
state: 'loading',
mode: 'create',
// True when this session began as a NEW document (create route); stays true after the header is
// saved and `mode` flips to 'edit', so the create flow shows Finish and an existing document opened
// for editing shows Save (issue #7403).
createFlow: false,
id: null,
error: null,
// The single header property a rejected save NAMED ("The 'Name' property is required"), else
Expand Down Expand Up @@ -105,6 +109,7 @@ document.addEventListener('alpine:init', () => {
const params = (window.PineconeRouter.context && window.PineconeRouter.context.params) || {};
this.id = params.id || null;
this.mode = this.id ? 'edit' : 'create';
this.createFlow = this.mode === 'create';
if (this.id) {
try {
const record = await App.services.api.get(this.apiPath + '/' + this.id);
Expand Down Expand Up @@ -524,5 +529,16 @@ document.addEventListener('alpine:init', () => {
// A document with unsaved header edits asks before it is abandoned (issue #7359); a clean one
// leaves silently, as before.
goBack() { this.guardExit(() => this.navigate('/partner/${name}')); },

// "Finish" on a saved document (issue #7403). Line items persist as they are added, so the only
// thing still unsaved is a header edit: save it, and only leave if the save succeeded (a failure
// sets this.error and keeps the user on the page). A clean header just returns to the list.
async finish() {
if (this.isDirty()) {
await this.save();
if (this.error) return;
}
this.goBack();
},
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,28 @@
<button x-h-button data-variant="negative" x-show="mode === 'edit'#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="deleteOpen = true"
x-text="T('$projectName:${tprefix}.defaults.delete', 'Delete')"></button>
<div class="flex-1"></div>
<!-- The button says what it will do: leaving a dirty form discards the edit (issue #7359). -->
<!-- Always an escape hatch, kept visible next to Create/Finish as in every other view: Cancel a
clean document, or Discard unsaved header edits (the label flips on isDirty(), issue #7359). -->
<button x-h-button data-variant="transparent" @click="goBack()" :disabled="saving"
x-text="isDirty() ? T('$projectName:${tprefix}.defaults.discard', 'Discard') : T('$projectName:${tprefix}.defaults.cancel', 'Cancel')"></button>
<button x-h-button data-variant="primary"#if($immutableStatusProperty || $immutableAlways || $periodLock) x-show="mutable"#end @click="save()" :disabled="saving || (mode === 'edit' &amp;&amp; !isDirty())">
<!-- Create a new header, then stay to add line items. -->
<button x-h-button data-variant="primary" x-show="mode !== 'edit'#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="save()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.save', 'Save')"></span>
<span x-text="T('$projectName:${tprefix}.defaults.create', 'Create')"></span>
</button>
<!-- Finish: ONLY while creating a new document (adding its line items after the header is saved).
Saves any header change and returns to the list - never a greyed Save on the new document
(issue #7403). -->
<button x-h-button data-variant="primary" x-show="mode === 'edit' &amp;&amp; createFlow#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="finish()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.finish', 'Finish')"></span>
</button>
<!-- Save: editing an EXISTING document saves any header change and returns to the list (issue
#7403); line items already persist as they are added. Reuses finish() (save-if-changed,
then back), so a failed save keeps the user on the page. -->
<button x-h-button data-variant="primary" x-show="mode === 'edit' &amp;&amp; !createFlow#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="finish()" :disabled="saving">
<span x-show="saving" x-h-spinner></span>
<span x-text="T('$projectName:${tprefix}.defaults.update', 'Save')"></span>
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ document.addEventListener('alpine:init', () => {

state: 'ready', // empty-form | loading | ready | saving | validation-error | save-failure | load-failure
mode: 'create',
// True when this session began as a NEW document (create route). Stays true after the header is
// saved and `mode` flips to 'edit' for adding line items, so the create flow shows Finish while an
// existing document opened for editing shows Save (issue #7403).
createFlow: false,
id: null,
record: {}, // the loaded master (drives the read-only totals footer + calculated header fields)

Expand Down Expand Up @@ -337,6 +341,9 @@ document.addEventListener('alpine:init', () => {
// Read the live hash to tell /preview from /edit (Pinecone's context can lag the hash).
const routePath = (window.location.hash || '').split('?')[0];
this.mode = this.id == null ? 'create' : (routePath.endsWith('/preview') ? 'preview' : 'edit');
// The create flow: either the /create route, or the /edit page the Create button just navigated to
// carrying ?created=1 (the header is saved, its line items are being added). Both show Finish.
this.createFlow = this.mode === 'create' || this.queryParam('created') === '1';

// The line-items child registered itself as a detail of this master; pick it by name.
this.itemsDef = App.detailsFor('${name}').find(d => d.entity === '${documentItemsEntity}') || null;
Expand Down Expand Up @@ -627,7 +634,9 @@ document.addEventListener('alpine:init', () => {
// The write landed, so there is nothing left to protect: clear the guard before the page
// navigates itself, or it would veto its own route change.
this.clearPristine();
window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(newId) + '/edit');
// ?created=1 marks this as the create flow across the route change, so the re-mounted page
// (now at /edit) still shows Finish while its line items are added, not Save (issue #7403).
window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(newId) + '/edit?created=1');
await this.loadHeader();
await this.loadItems();
}
Expand Down Expand Up @@ -1406,6 +1415,20 @@ document.addEventListener('alpine:init', () => {
// being asked about (issue #7359). A clean one leaves silently, as before.
backToList() { this.guardExit(() => window.PineconeRouter.navigate('/${name}')); },

// "Finish" on a saved document (issue #7403). Line items persist as they are added, so the only
// thing that can still be unsaved is a header edit: save it first, and only leave if the save
// succeeded - a failed validation keeps the user on the page with the error, never navigating on
// failure. A clean header just returns to the list. Not offered on create (that is the Create
// button, which stays on the document so line items can be added).
async finish() {
if (this.isPreview) return;
if (this.isDirty()) {
await this.save();
if (this.state !== 'ready') return;
}
window.PineconeRouter.navigate('/${name}');
},

// Preview -> the editable document for the same record.
goEdit() { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(this.id) + '/edit'); },
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,16 +529,13 @@
</div>
</template>

<!-- ===== Document footer: BPM user-task buttons on the LEFT; Cancel + Save/Create on the RIGHT ===== -->
<!-- ===== Document footer: utilities + Cancel/Discard on the LEFT; Create/Finish on the RIGHT
(workflow user-tasks live in the Process Inbox, not here - issue #7403) ===== -->
<div x-h-toolbar.footer>
#if($hasProcess)
<!-- Actionable BPM user-task buttons (edit only, hidden in read-only preview), left-aligned. -->
<template x-for="task in $store.processTasks.getTasks(form)" :key="task.id">
<button type="button" x-h-button data-variant="primary" x-show="!isPreview" @click="$store.processTasks.openTask(task)">
<svg role="presentation" x-h-lucide data-lucide="inbox"></svg><span x-text="$store.processTasks.taskName(task)"></span>
</button>
</template>
#end
<!-- Workflow user-tasks (Approve, Issue, ...) are NOT offered here: the editor footer is for
authoring the document, not advancing its lifecycle. The same tasks live in the Process
Inbox and the notification bell (both read /services/inbox/tasks), so nothing is lost by
keeping them out of the editing surface (issue #7403). -->
<div x-h-toolbar-spacer></div>
<span class="text-sm text-secondary-foreground" x-show="printError" x-text="printError"></span>
<!-- Print: renders the CMS template (Templates/${name}/Print/<lang>/) to PDF; asks for the
Expand Down Expand Up @@ -568,12 +565,27 @@
<button type="button" x-h-button data-variant="primary" x-show="isPreview#if($immutableStatusProperty || $immutableAlways || $periodLock) &amp;&amp; mutable#end" @click="goEdit()">
<svg role="presentation" x-h-lucide data-lucide="pencil"></svg><span x-text="T('$projectName:${tprefix}.defaults.edit', 'Edit')"></span>
</button>
<!-- The button says what it will do: leaving a dirty form discards the edit (issue #7359). -->
<!-- Always an escape hatch, kept visible next to Create/Finish as in every other view: Cancel a
clean document, or Discard unsaved header edits (the label flips on isDirty(), issue #7359). -->
<button type="button" x-h-button data-variant="transparent" x-show="!isPreview" @click="backToList()" :disabled="state === 'saving'" x-text="isDirty() ? T('$projectName:${tprefix}.defaults.discard', 'Discard') : T('$projectName:${tprefix}.defaults.cancel', 'Cancel')"></button>
<!-- Nothing to save on an unchanged document; a create is always offerable. -->
<button type="button" x-h-button data-variant="primary" x-show="!isPreview" :disabled="state === 'saving' || (isEdit &amp;&amp; !isDirty())" @click="save()">
<!-- Create a new header, then STAY on the document (edit mode) so line items can be added. -->
<button type="button" x-h-button data-variant="primary" x-show="!isPreview &amp;&amp; !isEdit" :disabled="state === 'saving'" @click="save()">
<span x-show="state === 'saving'" x-h-spinner></span>
<svg x-h-lucide role="presentation" data-lucide="save" x-show="state !== 'saving'"></svg><span x-text="T('$projectName:${tprefix}.defaults.create', 'Create')"></span>
</button>
<!-- Finish: ONLY while creating a new document (after its header is saved, its line items being
added). Line items save as you add them, so Finish saves any header change and returns to the
list - never a greyed Save on the new document (issue #7403). -->
<button type="button" x-h-button data-variant="primary" x-show="!isPreview &amp;&amp; isEdit &amp;&amp; createFlow" :disabled="state === 'saving'" @click="finish()">
<span x-show="state === 'saving'" x-h-spinner></span>
<svg x-h-lucide role="presentation" data-lucide="check" x-show="state !== 'saving'"></svg><span x-text="T('$projectName:${tprefix}.defaults.finish', 'Finish')"></span>
</button>
<!-- Save: editing an EXISTING document saves any header change and returns to the list (issue
#7403). Line items already persist as they are added, so this is the one primary exit; it
reuses finish() (save-if-changed, then back) so a failed check keeps the user on the page. -->
<button type="button" x-h-button data-variant="primary" x-show="!isPreview &amp;&amp; isEdit &amp;&amp; !createFlow" :disabled="state === 'saving'" @click="finish()">
<span x-show="state === 'saving'" x-h-spinner></span>
<svg x-h-lucide role="presentation" data-lucide="save" x-show="state !== 'saving'"></svg><span x-text="isEdit ? T('$projectName:${tprefix}.defaults.update', 'Save') : T('$projectName:${tprefix}.defaults.create', 'Create')"></span>
<svg x-h-lucide role="presentation" data-lucide="save" x-show="state !== 'saving'"></svg><span x-text="T('$projectName:${tprefix}.defaults.update', 'Save')"></span>
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,17 +457,12 @@
</div>

<!-- Action footer at the very bottom of the main column (below the detail panels), matching the
document view. BPM user-task buttons on the LEFT; Cancel + Save/Create on the RIGHT. Save is
type=button @click=save() because it lives outside the header <form>. -->
document view. Cancel + Save/Create on the RIGHT; workflow user-tasks live in the Process
Inbox, not here (issue #7403). Save is type=button @click=save() because it lives outside the
header <form>. -->
<div x-h-toolbar.footer>
#if($hasProcess)
<!-- Actionable BPM user-task buttons (edit only, hidden in read-only preview), left-aligned. -->
<template x-for="task in $store.processTasks.getTasks(form)" :key="task.id">
<button type="button" x-h-button data-variant="primary" x-show="!isPreview" @click="$store.processTasks.openTask(task)">
<svg role="presentation" x-h-lucide data-lucide="inbox"></svg><span x-text="$store.processTasks.taskName(task)"></span>
</button>
</template>
#end
<!-- Workflow user-tasks are NOT offered in the editor footer: the same tasks live in the Process
Inbox and the notification bell, so the footer is for editing the record only (issue #7403). -->
<div x-h-toolbar-spacer></div>
#if($hasPrint == "true")
<!-- Print: for a document master rendered through this shared form (a calendar/slots view that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"open": "Open",
"readOnly": "Read-only",
"update": "Update",
"finish": "Finish",
"delete": "Delete",
"cancel": "Cancel",
"discard": "Discard",
Expand Down
Loading