Skip to content
Merged
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 @@ -129,6 +129,11 @@ function detailPanel(def, masterId) {
const q = '?returnTo=' + encodeURIComponent(this.def.returnTo);
window.PineconeRouter.navigate('/' + this.def.entity + '/' + encodeURIComponent(row[this.def.primaryKey]) + '/edit' + q);
},
// Read-only view of the detail record (the routed form page in preview mode).
previewRow(row) {
const q = '?returnTo=' + encodeURIComponent(this.def.returnTo);
window.PineconeRouter.navigate('/' + this.def.entity + '/' + encodeURIComponent(row[this.def.primaryKey]) + '/preview' + q);
},

askDelete(row) { this.deleteTarget = row; this.deleteOpen = true; },
async confirmDelete() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

/*
* Document (header-items) editor for ${name}. One component serves both routes:
* /${name}/create -> create mode (header only; items enable once the header is saved)
* Document (header-items) editor for ${name}. One component serves all three routes:
* /${name}/create -> create mode (header only; items enable once the header is saved)
* /${name}/{id}/edit -> edit mode (loads the document, its items, and the totals)
* /${name}/{id}/preview -> preview mode (same data as edit, everything read-only; Print stays)
*
* Layout (see ${name}-document.html): the master's own fields as a header form (the `aggregate`
* totals are excluded here and shown in the footer), an inline-editable line-items table for the
Expand Down Expand Up @@ -157,9 +158,10 @@ document.addEventListener('alpine:init', () => {
apiPath: '/${javaPerspectiveName}/${name}Controller',

get isEdit() { return this.mode === 'edit'; },
get isPreview() { return this.mode === 'preview'; },
get errorCount() { return Object.keys(this.errors).filter(k => this.errors[k]).length; },
// Items can only be added once the document (header) exists, so we have a master id to bind them to.
get itemsEnabled() { return this.isEdit && this.id != null; },
get itemsEnabled() { return (this.isEdit || this.isPreview) && this.id != null; },
// Any OTHER composition child of this master (not the line-items entity) renders as a normal panel —
// like the line items, only once the document (header) exists (nothing to allocate against on Create).
get secondaryDetails() { return this.itemsEnabled ? App.detailsFor('${name}').filter(d => d.entity !== '${documentItemsEntity}') : []; },
Expand Down Expand Up @@ -232,7 +234,9 @@ document.addEventListener('alpine:init', () => {
async init() {
const params = (window.PineconeRouter.context && window.PineconeRouter.context.params) || {};
this.id = params.id ?? null;
this.mode = this.id != null ? 'edit' : 'create';
// 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 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 All @@ -250,7 +254,7 @@ document.addEventListener('alpine:init', () => {

await this.loadOptions();

if (this.isEdit) {
if (this.isEdit || this.isPreview) {
this.state = 'loading';
try {
await this.loadHeader();
Expand Down Expand Up @@ -383,6 +387,7 @@ document.addEventListener('alpine:init', () => {
},

async save() {
if (this.isPreview) return;
if (!this.validate()) {
this.state = 'validation-error';
this.scrollToSummary();
Expand Down Expand Up @@ -601,7 +606,7 @@ document.addEventListener('alpine:init', () => {

// ----- Add / edit line dialog ----------------------------------------------------------------
openRowDialog(row) {
if (!this.itemsEnabled) return;
if (!this.itemsEnabled || this.isPreview) return;
this.draftError = null;
this.draftMode = row ? 'edit' : 'create';
// Suppress the Depends-On watchers while the draft is (re)built - filling an existing row's
Expand Down Expand Up @@ -681,7 +686,7 @@ document.addEventListener('alpine:init', () => {
}
},

askDeleteRow(row) { this.deleteTarget = row; this.deleteOpen = true; },
askDeleteRow(row) { if (this.isPreview) return; this.deleteTarget = row; this.deleteOpen = true; },

async confirmDeleteRow() {
if (!this.deleteTarget) return;
Expand Down Expand Up @@ -720,5 +725,8 @@ document.addEventListener('alpine:init', () => {
},

backToList() { window.PineconeRouter.navigate('/${name}'); },

// Preview -> the editable document for the same record.
goEdit() { window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(this.id) + '/edit'); },
}));
}, { once: true });
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<!--
Generated by Eclipse Dirigible based on model and template.
Document (header-items) editor for ${name}. Rendered into #app on /${name}/create and
/${name}/{id}/edit; its root x-data binds the generated ${name}DocumentPage.
Header form (master fields, minus the aggregate totals) on top; once the document is saved the
inline-editable line-items table (${documentItemsEntity}, add/edit via dialog) and a right-aligned
totals footer appear; the Save / Create action sits at the very bottom.
Document (header-items) editor for ${name}. Rendered into #app on /${name}/create,
/${name}/{id}/edit and /${name}/{id}/preview; its root x-data binds the generated
${name}DocumentPage. Header form (master fields, minus the aggregate totals) on top; once the
document is saved the inline-editable line-items table (${documentItemsEntity}, add/edit via
dialog) and a right-aligned totals footer appear; the Save / Create action sits at the very
bottom. Preview renders the same document fully read-only (no item/allocation editing, no BPM
buttons, footer swapped to Close + Edit) - Print stays available.
-->
<div x-data="${name}DocumentPage" class="vbox size-full">
## Document roles: the DOCUMENT_NUMBER field shows in the title (e.g. "SALES INVOICE 00001231"); the
Expand All @@ -23,15 +25,15 @@
directive injects font-semibold, which the label must NOT have; only the number is bold. -->
<div class="hbox items-baseline gap-2 shrink-0">
<span class="text-2xl font-normal tracking-tight" x-text="T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}').toUpperCase()"></span>
<span class="text-2xl font-bold tracking-tight" x-show="isEdit && form.${docNumberProp}" x-text="form.${docNumberProp}"></span>
<span class="text-2xl font-bold tracking-tight" x-show="(isEdit || isPreview) && form.${docNumberProp}" x-text="form.${docNumberProp}"></span>
</div>
#else
<span x-h-toolbar-title class="shrink-0" x-text="isEdit ? T('$projectName:${tprefix}.defaults.formHeadUpdate', 'Edit ${documentLabel}', { name: T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}') }) : T('$projectName:${tprefix}.defaults.formHeadCreate', 'New ${documentLabel}', { name: T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}') })"></span>
<span x-h-toolbar-title class="shrink-0" x-text="isPreview ? T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}') : (isEdit ? T('$projectName:${tprefix}.defaults.formHeadUpdate', 'Edit ${documentLabel}', { name: T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}') }) : T('$projectName:${tprefix}.defaults.formHeadCreate', 'New ${documentLabel}', { name: T('$projectName:${tprefix}.t.${dataName}', '${documentLabel}') }))"></span>
#end
<div x-h-toolbar-spacer></div>
#if($docStatusProp != "")
<!-- Status pill is right-aligned: the spacer above pushes it (and the Back button) to the far end. -->
<span x-h-badge class="shrink-0" :data-variant="statusVariant(statusText())" x-show="isEdit && statusText()" x-text="statusText()"></span>
<span x-h-badge class="shrink-0" :data-variant="statusVariant(statusText())" x-show="(isEdit || isPreview) && statusText()" x-text="statusText()"></span>
#end
<button type="button" x-h-button data-variant="transparent" @click="backToList()" x-text="T('$projectName:${tprefix}.defaults.backToList', 'Back to list')"></button>
</div>
Expand Down Expand Up @@ -63,6 +65,9 @@
</div>
</div>

<!-- The fieldset (display: contents keeps the grid layout intact) disables EVERY header
control at once in preview mode - the one switch that guarantees read-only. -->
<fieldset :disabled="isPreview" style="display: contents;">
<div class="grid grid-cols-1 sm:grid-cols-12 gap-4">
#foreach($property in $properties)
## Read-only / system fields (ProcessId, audit, uuid, any field marked read-only) render in the
Expand All @@ -89,7 +94,7 @@
<!-- Non-setting association: combobox + Add new (opens the target's own create page in an iframe
dialog) + Refresh, all on one full-width row. -->
<div class="hbox gap-1 items-center">
<div x-h-select data-filter="contains" class="grow">
<div x-h-select data-filter="contains" class="grow" :data-disabled="isPreview">
<input x-h-select-input id="f_${property.name}" x-model="form.${property.name}" :aria-invalid="!!errors.${property.name}" :placeholder="T('$projectName:${tprefix}.messages.inputSelect', 'Select ${label}...', { name: T('$projectName:${tprefix}.t.${property.dataName}', '${label}') })" />
<div x-h-select-content>
<div x-h-select-search></div>
Expand All @@ -98,11 +103,11 @@
</template>
</div>
</div>
<button type="button" x-h-button data-variant="outline" class="shrink-0" @click="addRelated('${property.name}', '${property.widgetDropdownControllerUrl}', '${property.relationshipEntityName}')"><i data-lucide="plus" class="size-4"></i> New ${property.relationshipEntityName}</button>
<button type="button" x-h-button data-variant="transparent" class="shrink-0" aria-label="Refresh" @click="loadOptions()"><i data-lucide="refresh-cw" class="size-4"></i></button>
<button type="button" x-h-button data-variant="outline" class="shrink-0" x-show="!isPreview" @click="addRelated('${property.name}', '${property.widgetDropdownControllerUrl}', '${property.relationshipEntityName}')"><i data-lucide="plus" class="size-4"></i> New ${property.relationshipEntityName}</button>
<button type="button" x-h-button data-variant="transparent" class="shrink-0" x-show="!isPreview" aria-label="Refresh" @click="loadOptions()"><i data-lucide="refresh-cw" class="size-4"></i></button>
</div>
#else
<div x-h-select data-filter="contains"#if($readonly) data-disabled="true"#end>
<div x-h-select data-filter="contains"#if($readonly) data-disabled="true"#else :data-disabled="isPreview"#end>
<input x-h-select-input id="f_${property.name}" x-model="form.${property.name}" :aria-invalid="!!errors.${property.name}" :placeholder="T('$projectName:${tprefix}.messages.inputSelect', 'Select ${label}...', { name: T('$projectName:${tprefix}.t.${property.dataName}', '${label}') })" />
<div x-h-select-content>
<div x-h-select-search></div>
Expand Down Expand Up @@ -134,6 +139,7 @@
#end
#end
</div>
</fieldset>

</form>

Expand All @@ -142,15 +148,15 @@
<div x-h-toolbar data-variant="transparent">
<span x-h-toolbar-title class="shrink-0">${documentItemsLabel}</span>
<div x-h-toolbar-spacer></div>
<button type="button" x-h-button data-variant="primary" data-size="sm" @click="openRowDialog(null)"><i role="img" data-lucide="plus"></i><span x-text="T('$projectName:${tprefix}.defaults.add', 'Add')"></span></button>
<button type="button" x-h-button data-variant="primary" data-size="sm" x-show="!isPreview" @click="openRowDialog(null)"><i role="img" data-lucide="plus"></i><span x-text="T('$projectName:${tprefix}.defaults.add', 'Add')"></span></button>
</div>
<div x-show="itemsError" x-h-alert data-variant="negative" role="alert"><div x-h-alert-description x-text="itemsError"></div></div>

<table x-h-table data-borders="rows" class="w-full">
<thead x-h-table-header>
<tr x-h-table-row>
<template x-for="col in editColumns" :key="col.name"><th x-h-table-head scope="col" :class="col.number ? 'text-right' : ''" x-text="T(col.tkey, col.label)"></th></template>
<th x-h-table-head scope="col"></th>
<th x-h-table-head scope="col" x-show="!isPreview"></th>
</tr>
</thead>
<tbody x-h-table-body>
Expand All @@ -159,7 +165,7 @@
<template x-for="col in editColumns" :key="col.name">
<td x-h-table-cell :class="col.number ? 'text-right' : ''" x-text="cellDisplay(col, row)"></td>
</template>
<td x-h-table-cell data-row-actions @click.stop>
<td x-h-table-cell data-row-actions x-show="!isPreview" @click.stop>
<button x-h-menu-trigger.dropdown x-h-table-cell-button aria-label="Row actions">
<i role="img" data-lucide="ellipsis"></i>
</button>
Expand All @@ -172,7 +178,7 @@
</tr>
</template>
<tr x-show="items.length === 0">
<td x-h-table-cell :colspan="editColumns.length + 1" class="text-center" x-text="T('$projectName:${tprefix}.messages.noData', 'No line items yet.')"></td>
<td x-h-table-cell :colspan="editColumns.length + (isPreview ? 0 : 1)" class="text-center" x-text="T('$projectName:${tprefix}.messages.noData', 'No line items yet.')"></td>
</tr>
</tbody>
</table>
Expand Down Expand Up @@ -200,10 +206,10 @@
correction is remove-the-wrong-row + add-the-right-one). Add/create returns to THIS document
(returnTo override) rather than the detail's default list. Only once the document exists. ===== -->
<template x-for="d in secondaryDetails" :key="d.entity + ':' + id">
<div x-data="detailPanel({...d, returnTo: '/${name}/' + id + '/edit'}, id)" x-h-card class="w-full">
<div x-data="detailPanel({...d, returnTo: '/${name}/' + id + (isPreview ? '/preview' : '/edit')}, id)" x-h-card class="w-full">
<div x-h-card-header>
<h3 x-h-card-title x-text="T(d.tkey, d.label)"></h3>
<div x-h-card-action x-show="masterId">
<div x-h-card-action x-show="masterId && !isPreview">
<button x-h-button data-variant="primary" data-size="sm" @click="addRow()"><i role="img" data-lucide="plus"></i><span x-text="T('$projectName:${tprefix}.defaults.add', 'Add')"></span></button>
</div>
</div>
Expand All @@ -218,7 +224,7 @@
<template x-for="col in def.columns" :key="col.name">
<th x-h-table-head scope="col" :class="col.number ? 'text-right' : ''" x-text="T(col.tkey, col.label || col.name)"></th>
</template>
<th x-h-table-head scope="col"></th>
<th x-h-table-head scope="col" x-show="!isPreview"></th>
</tr>
</thead>
<tbody x-h-table-body>
Expand All @@ -227,7 +233,7 @@
<template x-for="col in def.columns" :key="col.name">
<td x-h-table-cell :class="col.number ? 'text-right' : ''" x-text="cellValue(col, row)"></td>
</template>
<td x-h-table-cell data-row-actions>
<td x-h-table-cell data-row-actions x-show="!isPreview">
<button x-h-button data-variant="transparent" data-size="sm" aria-label="Delete" @click="askDelete(row)"><i role="img" data-lucide="trash-2"></i></button>
</td>
</tr>
Expand Down Expand Up @@ -257,36 +263,42 @@
<!-- ===== Document footer: BPM user-task buttons on the LEFT; Cancel + Save/Create on the RIGHT ===== -->
<div x-h-toolbar.footer>
#if($hasProcess)
<!-- Actionable BPM user-task buttons (edit only), left-aligned. -->
<!-- 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" @click="$store.processTasks.openTask(task)">
<button type="button" x-h-button data-variant="primary" x-show="!isPreview" @click="$store.processTasks.openTask(task)">
<i role="img" data-lucide="inbox"></i><span x-text="task.name"></span>
</button>
</template>
#end
<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 language when several exist -->
<button type="button" x-h-button data-variant="outline" x-show="isEdit" :disabled="printBusy" @click="openPrint()">
<!-- Print: renders the CMS template (Templates/${name}/Print/<lang>/) to PDF; asks for the
language when several exist. Stays available in preview (printing is read-only). -->
<button type="button" x-h-button data-variant="outline" x-show="isEdit || isPreview" :disabled="printBusy" @click="openPrint()">
<span x-show="printBusy" x-h-spinner></span>
<i role="img" data-lucide="printer" x-show="!printBusy"></i><span x-text="T('$projectName:${tprefix}.defaults.print', 'Print')"></span>
</button>
<button type="button" x-h-button data-variant="transparent" @click="backToList()" :disabled="state === 'saving'" x-text="T('$projectName:${tprefix}.defaults.cancel', 'Cancel')"></button>
<button type="button" x-h-button data-variant="primary" :disabled="state === 'saving'" @click="save()">
<!-- Preview footer: Close + Edit instead of Cancel + Save. -->
<button type="button" x-h-button data-variant="transparent" x-show="isPreview" @click="backToList()" x-text="T('$projectName:${tprefix}.defaults.close', 'Close')"></button>
<button type="button" x-h-button data-variant="primary" x-show="isPreview" @click="goEdit()">
<i role="img" data-lucide="pencil"></i><span x-text="T('$projectName:${tprefix}.defaults.edit', 'Edit')"></span>
</button>
<button type="button" x-h-button data-variant="transparent" x-show="!isPreview" @click="backToList()" :disabled="state === 'saving'" x-text="T('$projectName:${tprefix}.defaults.cancel', 'Cancel')"></button>
<button type="button" x-h-button data-variant="primary" x-show="!isPreview" :disabled="state === 'saving'" @click="save()">
<span x-show="state === 'saving'" x-h-spinner></span>
<i role="img" data-lucide="save" x-show="state !== 'saving'"></i><span x-text="isEdit ? T('$projectName:${tprefix}.defaults.update', 'Save') : T('$projectName:${tprefix}.defaults.create', 'Create')"></span>
</button>
</div>
</div>

<!-- Right sidebar: read-only / audit fields (audit, ProcessId, uuid, read-only), single column,
top-to-bottom (edit only). -->
top-to-bottom (edit/preview only). -->
#set($hasReadOnly = false)
#foreach($property in $properties)
#if($property.isReadOnlyProperty == "true" || ($property.auditType && $property.auditType != "NONE"))#set($hasReadOnly = true)#end
#end
#if($hasReadOnly)
<div class="vbox gap-4 shrink-0" style="width: 18rem;" x-show="isEdit">
<div class="vbox gap-4 shrink-0" style="width: 18rem;" x-show="isEdit || isPreview">
<div x-h-card>
<div x-h-card-header><h3 x-h-card-title x-text="T('$projectName:${tprefix}.defaults.details', 'Details')"></h3></div>
<div x-h-card-content>
Expand Down
Loading
Loading