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 @@ -16,10 +16,11 @@
* passing the panel definition and the selected master id. The panel lists that detail's
* rows filtered to the master via the controller's `?<masterEntityId>=<id>` query (which
* the generated REST controller supports for MANAGE_DETAILS/LIST_DETAILS), supports delete,
* and routes create/edit to the detail's own generated form page (FK + returnTo preset).
* and opens create/edit/preview in the shared related-record iframe dialog (never a main-pane
* navigation - the master form may hold unsaved edits).
*
* `def` shape (from App.registerDetail): { entity, apiPath, masterEntityId, label,
* columns: [{ name }], returnTo }. apiPath is relative to App.config.restBase (the api client
* columns: [{ name }] }. apiPath is relative to App.config.restBase (the api client
* prepends it), so detail calls pass no baseUrl override.
*
* A def carrying `calendar: { start, end?, title?, color?, view, range }` (a composition child
Expand Down Expand Up @@ -137,20 +138,29 @@ function detailPanel(def, masterId) {
this.refreshIcons();
},

// Create/edit route to the detail's own form page, carrying the FK + a returnTo back here.
// The detail's own form page, opened in the shared related-record iframe DIALOG - never a
// main-pane navigation. The parent form may hold unsaved edits, and navigating away silently
// discards them (observed live: fill a record, add a child, come back to empty fields). The
// child form runs the same SPA embedded (?embedded=1 hides its chrome) in dialog mode
// (dialog=1 - save/cancel post messages to the opener instead of navigating); on save the
// panel reloads its rows while the parent form keeps its state.
openForm(route, title) {
Alpine.store('related').create(window.location.pathname + '?embedded=1#' + route, title, () => this.load());
},
addRow() {
const q = '?' + encodeURIComponent(this.def.masterEntityId) + '=' + encodeURIComponent(this.masterId)
+ '&returnTo=' + encodeURIComponent(this.def.returnTo);
window.PineconeRouter.navigate('/' + this.def.entity + '/create' + q);
+ '&embedded=1&dialog=1';
this.openForm('/' + this.def.entity + '/create' + q,
(window.T ? T('application-core:shell.related.addNew', 'Add new') : 'Add new'));
},
editRow(row) {
const q = '?returnTo=' + encodeURIComponent(this.def.returnTo);
window.PineconeRouter.navigate('/' + this.def.entity + '/' + encodeURIComponent(row[this.def.primaryKey]) + '/edit' + q);
this.openForm('/' + this.def.entity + '/' + encodeURIComponent(row[this.def.primaryKey]) + '/edit?embedded=1&dialog=1',
(window.T ? T(this.def.tkey, this.def.label) : this.def.label));
},
// 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);
this.openForm('/' + this.def.entity + '/' + encodeURIComponent(row[this.def.primaryKey]) + '/preview?embedded=1&dialog=1',
(window.T ? T(this.def.tkey, this.def.label) : this.def.label));
},

// --- embedded calendar (calendar defs only) -------------------------------------------------
Expand Down Expand Up @@ -220,24 +230,26 @@ function detailPanel(def, masterId) {
},
// Event click -> edit the child; empty-day click -> create one with the master FK AND the
// clicked date preset (the shared form presets any create query param whose name matches).
// Both open the shared iframe dialog (openForm) so the parent's unsaved edits survive.
onEventClick(e) {
const id = e && e.detail && e.detail.event ? e.detail.event.id : null;
if (!id) return;
const q = '?returnTo=' + encodeURIComponent(this.def.returnTo);
window.PineconeRouter.navigate('/' + this.def.entity + '/' + encodeURIComponent(id) + '/edit' + q);
this.openForm('/' + this.def.entity + '/' + encodeURIComponent(id) + '/edit?embedded=1&dialog=1',
(window.T ? T(this.def.tkey, this.def.label) : this.def.label));
},
onDateClick(e) {
const cal = this.def.calendar;
let q = '?' + encodeURIComponent(this.def.masterEntityId) + '=' + encodeURIComponent(this.masterId)
+ '&returnTo=' + encodeURIComponent(this.def.returnTo);
+ '&embedded=1&dialog=1';
const d = e && e.detail ? e.detail.date : null;
if (d instanceof Date && !isNaN(d.getTime())) {
const p = n => String(n).padStart(2, '0');
let val = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
if (e.detail.time) val += 'T' + e.detail.time;
q += '&' + encodeURIComponent(cal.start) + '=' + encodeURIComponent(val);
}
window.PineconeRouter.navigate('/' + this.def.entity + '/create' + q);
this.openForm('/' + this.def.entity + '/create' + q,
(window.T ? T('application-core:shell.related.addNew', 'Add new') : 'Add new'));
},

// --- files panel (files defs only) ----------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ function baseFormPage() {
} catch (e) { /* cross-origin / standalone: nothing to notify */ }
},

// Tell the hosting dialog an EDIT saved (a detail-panel dialog's update) so it can close and
// reload the panel - navigating this iframe to a list nobody sees would strand the dialog open.
emitSaved(id) {
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'harmonia.entity.updated', id: id }, '*');
}
} catch (e) { /* cross-origin / standalone: nothing to notify */ }
},

// Ask the hosting dialog to close (embedded Cancel) instead of navigating this iframe to a list.
emitClose() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ document.addEventListener('alpine:init', () => {
});
});

// The embedded create page posts a message to its parent: 'harmonia.entity.created' (with id) on
// save, or 'harmonia.dialog.cancel' when the user cancels. Route both to the related store.
// The embedded form posts a message to its parent: 'harmonia.entity.created' (with id) when a
// create saves, 'harmonia.entity.updated' when a detail-panel dialog's edit saves, or
// 'harmonia.dialog.cancel' when the user cancels. Created and updated take the same path - close
// the dialog and hand the id to the opener's callback (select the new record / reload the panel).
window.addEventListener('message', (event) => {
const data = event && event.data;
if (!data) return;
try {
const store = window.Alpine && Alpine.store('related');
if (!store || !store.open) return;
if (data.type === 'harmonia.entity.created') store.handleCreated(data.id);
if (data.type === 'harmonia.entity.created' || data.type === 'harmonia.entity.updated') store.handleCreated(data.id);
else if (data.type === 'harmonia.dialog.cancel') store.close();
} catch (e) { /* Alpine not ready / no store */ }
});
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,12 @@ document.addEventListener('alpine:init', () => {
const payload = this.toPayload();
if (this.isEdit) {
await App.services.api.put(this.apiPath + '/' + encodeURIComponent(this.id), payload);
// Opened as a detail-panel iframe dialog: report the save to the opener (it closes the
// dialog and reloads the panel) instead of navigating this iframe to a list nobody sees.
if (this.isDialog) {
this.emitSaved(this.id);
return;
}
} else {
const created = await App.services.api.post(this.apiPath, payload);
// Only when opened as an FK "Add" iframe dialog: report the new record to the opener and stop
Expand Down Expand Up @@ -620,8 +626,14 @@ document.addEventListener('alpine:init', () => {
this.navigateBack('/${name}');
},

// Preview -> the editable form for the same record (returnTo carried over).
// Preview -> the editable form for the same record (returnTo carried over). Inside a detail
// dialog the edit stays IN the dialog (embedded + dialog mode carried), so its save still
// reports to the opener instead of navigating the iframe to a list.
goEdit() {
if (this.isDialog) {
window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(this.id) + '/edit?embedded=1&dialog=1');
return;
}
const ret = this.returnToParam();
window.PineconeRouter.navigate('/${name}/' + encodeURIComponent(this.id) + '/edit' + (ret ? '?returnTo=' + encodeURIComponent(ret) : ''));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ App.registerDetail('${masterEntity}', {
apiPath: '/${javaPerspectiveName}/${name}Controller',
masterEntityId: '${masterEntityId}',
primaryKey: '${primaryKeysString}',
returnTo: '/${masterEntity}',
#if($detailCalendar)
// Rendered as an embedded x-h-calendar panel on the master page (intent view: calendar on a
// composition child); the shared detailPanel maps the rows to events by these properties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,16 @@ void harmonia_form_page_generates_the_depends_on_runtime() {
String copyRegister = contentOf("gen/orders/js/components/pages/Order/OrderCopy.detail.js");
assertTrue(copyRegister.contains("files: { readOnly: true }"),
"a function: Snapshot child must register as a read-only files def, got: " + copyRegister);

// Detail-panel children open in the shared iframe DIALOG, never a main-pane navigation - a
// navigation would discard the master form's unsaved edits (observed live: fill a record,
// add a child, come back to empty fields). The register therefore carries no returnTo
// route, and the generated form page reports a dialog-mode EDIT save to the opener.
assertFalse(copyRegister.contains("returnTo"),
"detail rows open in a dialog - the register must not carry a main-pane return route");
String customerFormPage = contentOf("gen/orders/js/components/pages/Customer/CustomerFormPage.js");
assertTrue(customerFormPage.contains("this.emitSaved(this.id)"),
"a dialog-mode edit save must report to the opener instead of navigating the iframe to a list");
String documentView = contentOf("gen/orders/views/Order/Order-document.html");
assertTrue(documentView.contains("openHref(row)"),
"the files panel rows must offer the inline Open action for stored snapshot versions");
Expand Down
Loading