@ketvietlab/ketsuite 0.1.20 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +9 -3
- package/dist/cli.js.map +1 -1
- package/dist/demo/crm.d.ts +3 -0
- package/dist/demo/crm.d.ts.map +1 -0
- package/dist/demo/crm.js +418 -0
- package/dist/demo/crm.js.map +1 -0
- package/dist/demo/partner.d.ts +8 -0
- package/dist/demo/partner.d.ts.map +1 -0
- package/dist/demo/partner.js +179 -0
- package/dist/demo/partner.js.map +1 -0
- package/dist/demo/product.d.ts +25 -0
- package/dist/demo/product.d.ts.map +1 -0
- package/dist/demo/product.js +579 -0
- package/dist/demo/product.js.map +1 -0
- package/dist/demo/purchase.d.ts +4 -0
- package/dist/demo/purchase.d.ts.map +1 -0
- package/dist/demo/purchase.js +58 -0
- package/dist/demo/purchase.js.map +1 -0
- package/dist/demo/sale.d.ts +4 -0
- package/dist/demo/sale.d.ts.map +1 -0
- package/dist/demo/sale.js +52 -0
- package/dist/demo/sale.js.map +1 -0
- package/dist/demo/seed.d.ts +14 -0
- package/dist/demo/seed.d.ts.map +1 -0
- package/dist/demo/seed.js +88 -0
- package/dist/demo/seed.js.map +1 -0
- package/dist/demo/stock.d.ts +7 -0
- package/dist/demo/stock.d.ts.map +1 -0
- package/dist/demo/stock.js +26 -0
- package/dist/demo/stock.js.map +1 -0
- package/dist/demo/user.d.ts +4 -0
- package/dist/demo/user.d.ts.map +1 -0
- package/dist/demo/user.js +41 -0
- package/dist/demo/user.js.map +1 -0
- package/dist/demo/util.d.ts +7 -0
- package/dist/demo/util.d.ts.map +1 -0
- package/dist/demo/util.js +19 -0
- package/dist/demo/util.js.map +1 -0
- package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
- package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs.map +3 -3
- package/dist/modules/backend/design/content.css +9 -9
- package/dist/modules/backend/design/controls.css +5 -5
- package/dist/modules/backend/design/forms.css +12 -5
- package/dist/modules/backend/design/foundation.css +3 -3
- package/dist/modules/backend/design/lists.css +23 -24
- package/dist/modules/backend/design/user-workflow.css +2 -2
- package/dist/modules/backend/screen.d.ts.map +1 -1
- package/dist/modules/backend/screen.js +11 -2
- package/dist/modules/backend/screen.js.map +1 -1
- package/dist/modules/crm/functions/access.d.ts +3 -0
- package/dist/modules/crm/functions/access.d.ts.map +1 -0
- package/dist/modules/crm/functions/access.js +89 -0
- package/dist/modules/crm/functions/access.js.map +1 -0
- package/dist/modules/crm/functions/activities.d.ts +3 -0
- package/dist/modules/crm/functions/activities.d.ts.map +1 -0
- package/dist/modules/crm/functions/activities.js +174 -0
- package/dist/modules/crm/functions/activities.js.map +1 -0
- package/dist/modules/crm/functions/cases.d.ts +3 -0
- package/dist/modules/crm/functions/cases.d.ts.map +1 -0
- package/dist/modules/crm/functions/cases.js +585 -0
- package/dist/modules/crm/functions/cases.js.map +1 -0
- package/dist/modules/crm/functions/configuration.d.ts +3 -0
- package/dist/modules/crm/functions/configuration.d.ts.map +1 -0
- package/dist/modules/crm/functions/configuration.js +331 -0
- package/dist/modules/crm/functions/configuration.js.map +1 -0
- package/dist/modules/crm/functions/gamification.d.ts +3 -0
- package/dist/modules/crm/functions/gamification.d.ts.map +1 -0
- package/dist/modules/crm/functions/gamification.js +58 -0
- package/dist/modules/crm/functions/gamification.js.map +1 -0
- package/dist/modules/crm/functions/index.d.ts +4 -0
- package/dist/modules/crm/functions/index.d.ts.map +1 -0
- package/dist/modules/crm/functions/index.js +33 -0
- package/dist/modules/crm/functions/index.js.map +1 -0
- package/dist/modules/crm/functions/modal-context.d.ts +3 -0
- package/dist/modules/crm/functions/modal-context.d.ts.map +1 -0
- package/dist/modules/crm/functions/modal-context.js +248 -0
- package/dist/modules/crm/functions/modal-context.js.map +1 -0
- package/dist/modules/crm/functions/previews.d.ts +3 -0
- package/dist/modules/crm/functions/previews.d.ts.map +1 -0
- package/dist/modules/crm/functions/previews.js +20 -0
- package/dist/modules/crm/functions/previews.js.map +1 -0
- package/dist/modules/crm/functions/shared.d.ts +31 -0
- package/dist/modules/crm/functions/shared.d.ts.map +1 -0
- package/dist/modules/crm/functions/shared.js +194 -0
- package/dist/modules/crm/functions/shared.js.map +1 -0
- package/dist/modules/crm/index.d.ts +1 -1
- package/dist/modules/crm/index.d.ts.map +1 -1
- package/dist/modules/crm/index.js +2 -2
- package/dist/modules/crm/index.js.map +1 -1
- package/dist/modules/crm/messages.d.ts +4 -1
- package/dist/modules/crm/messages.d.ts.map +1 -1
- package/dist/modules/crm/messages.js +6 -0
- package/dist/modules/crm/messages.js.map +1 -1
- package/dist/modules/crm/record-modal-labels.d.ts +9 -0
- package/dist/modules/crm/record-modal-labels.d.ts.map +1 -0
- package/dist/modules/crm/record-modal-labels.js +36 -0
- package/dist/modules/crm/record-modal-labels.js.map +1 -0
- package/dist/modules/crm/types.d.ts +5 -0
- package/dist/modules/crm/types.d.ts.map +1 -1
- package/dist/modules/crm/types.js +9 -0
- package/dist/modules/crm/types.js.map +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +352 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +2 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +7 -0
- package/dist/modules/crm_backend/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/index.js +5 -1
- package/dist/modules/crm_backend/index.js.map +1 -1
- package/dist/modules/crm_backend/islands.d.ts +27 -0
- package/dist/modules/crm_backend/islands.d.ts.map +1 -1
- package/dist/modules/crm_backend/islands.js +22 -0
- package/dist/modules/crm_backend/islands.js.map +1 -1
- package/dist/modules/crm_backend/messages.d.ts +30 -0
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +30 -0
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts +55 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts.map +1 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.js +601 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.js.map +1 -0
- package/dist/modules/crm_backend/routes.d.ts.map +1 -1
- package/dist/modules/crm_backend/routes.js +107 -368
- package/dist/modules/crm_backend/routes.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts +24 -32
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +183 -274
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/index.d.ts +2 -2
- package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/index.js +1 -1
- package/dist/modules/crm_backend/screens/index.js.map +1 -1
- package/dist/modules/product/functions.d.ts +14 -1
- package/dist/modules/product/functions.d.ts.map +1 -1
- package/dist/modules/product/functions.js +36 -31
- package/dist/modules/product/functions.js.map +1 -1
- package/dist/modules/product/product-modal-context.d.ts +3 -0
- package/dist/modules/product/product-modal-context.d.ts.map +1 -0
- package/dist/modules/product/product-modal-context.js +171 -0
- package/dist/modules/product/product-modal-context.js.map +1 -0
- package/dist/modules/product_backend/client/product-modal.d.mts +143 -0
- package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -0
- package/dist/modules/product_backend/client/product-modal.mjs +2 -0
- package/dist/modules/product_backend/client/product-modal.mjs.map +7 -0
- package/dist/modules/product_backend/index.d.ts.map +1 -1
- package/dist/modules/product_backend/index.js +16 -0
- package/dist/modules/product_backend/index.js.map +1 -1
- package/dist/modules/product_backend/islands.d.ts +1 -0
- package/dist/modules/product_backend/islands.d.ts.map +1 -1
- package/dist/modules/product_backend/islands.js +9 -0
- package/dist/modules/product_backend/islands.js.map +1 -1
- package/dist/modules/product_backend/modal/product-modal-view.d.ts +43 -0
- package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -0
- package/dist/modules/product_backend/modal/product-modal-view.js +602 -0
- package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -0
- package/dist/modules/product_backend/routes.d.ts.map +1 -1
- package/dist/modules/product_backend/routes.js +44 -330
- package/dist/modules/product_backend/routes.js.map +1 -1
- package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/detail.js +6 -108
- package/dist/modules/product_backend/screens/detail.js.map +1 -1
- package/dist/modules/product_backend/screens/index.d.ts +0 -1
- package/dist/modules/product_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/index.js +0 -1
- package/dist/modules/product_backend/screens/index.js.map +1 -1
- package/dist/modules/product_backend/screens/list.d.ts +3 -1
- package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/list.js +6 -5
- package/dist/modules/product_backend/screens/list.js.map +1 -1
- package/dist/modules/user/authorization.d.ts.map +1 -1
- package/dist/modules/user/authorization.js +114 -0
- package/dist/modules/user/authorization.js.map +1 -1
- package/dist/modules/user/functions.d.ts.map +1 -1
- package/dist/modules/user/functions.js +2 -0
- package/dist/modules/user/functions.js.map +1 -1
- package/dist/modules/user/user-modal-context.d.ts +3 -0
- package/dist/modules/user/user-modal-context.d.ts.map +1 -0
- package/dist/modules/user/user-modal-context.js +151 -0
- package/dist/modules/user/user-modal-context.js.map +1 -0
- package/dist/modules/user_backend/client/user-modal.d.mts +72 -0
- package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -0
- package/dist/modules/user_backend/client/user-modal.mjs +2 -0
- package/dist/modules/user_backend/client/user-modal.mjs.map +7 -0
- package/dist/modules/user_backend/index.d.ts.map +1 -1
- package/dist/modules/user_backend/index.js +9 -0
- package/dist/modules/user_backend/index.js.map +1 -1
- package/dist/modules/user_backend/islands.d.ts +15 -0
- package/dist/modules/user_backend/islands.d.ts.map +1 -0
- package/dist/modules/user_backend/islands.js +14 -0
- package/dist/modules/user_backend/islands.js.map +1 -0
- package/dist/modules/user_backend/messages.d.ts +30 -0
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +30 -0
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/modal/user-modal-view.d.ts +26 -0
- package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -0
- package/dist/modules/user_backend/modal/user-modal-view.js +320 -0
- package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -0
- package/dist/modules/user_backend/routes.d.ts.map +1 -1
- package/dist/modules/user_backend/routes.js +15 -3
- package/dist/modules/user_backend/routes.js.map +1 -1
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +8 -0
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/client/activity.css +6 -6
- package/dist/ui/client/calendar.css +6 -6
- package/dist/ui/client/flow-app.css +1 -1
- package/dist/ui/client/mail.css +6 -6
- package/dist/ui/client/record-modal-form.d.ts +67 -0
- package/dist/ui/client/record-modal-form.d.ts.map +1 -0
- package/dist/ui/client/record-modal-form.js +45 -0
- package/dist/ui/client/record-modal-form.js.map +1 -0
- package/dist/ui/client/record-modal.d.ts +97 -6
- package/dist/ui/client/record-modal.d.ts.map +1 -1
- package/dist/ui/client/record-modal.js +330 -64
- package/dist/ui/client/record-modal.js.map +1 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +5 -3
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/nav.d.ts +7 -0
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +1 -1
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/record-modal.d.ts +15 -0
- package/dist/ui/record-modal.d.ts.map +1 -1
- package/dist/ui/record-modal.js +12 -0
- package/dist/ui/record-modal.js.map +1 -1
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +1 -1
- package/dist/ui/table.js.map +1 -1
- package/package.json +4 -4
- package/dist/modules/crm/functions.d.ts +0 -10
- package/dist/modules/crm/functions.d.ts.map +0 -1
- package/dist/modules/crm/functions.js +0 -1394
- package/dist/modules/crm/functions.js.map +0 -1
- package/dist/modules/product_backend/screens/create.d.ts +0 -16
- package/dist/modules/product_backend/screens/create.d.ts.map +0 -1
- package/dist/modules/product_backend/screens/create.js +0 -86
- package/dist/modules/product_backend/screens/create.js.map +0 -1
|
@@ -11,8 +11,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@ketvietlab/k
|
|
|
11
11
|
// Views are render-pure: they read the context and return design-system markup.
|
|
12
12
|
// They never fetch, never touch history and never query the document.
|
|
13
13
|
import { signal } from '@ketvietlab/ketjs-view';
|
|
14
|
-
import { LoadingState, ModalSheet, Notice,
|
|
15
|
-
import { RECORD_PARAM, RECORD_TAB_PARAM, readRecordModalTarget, recordModalClosedHref, recordModalHost, recordModalHref, } from '../record-modal.js';
|
|
14
|
+
import { Button, LoadingState, ModalSheet, Notice, TabbedView } from '@ketvietlab/design-system';
|
|
15
|
+
import { RECORD_NEW_ID, RECORD_PARAM, RECORD_TAB_PARAM, readRecordModalTarget, recordModalClosedHref, recordModalCreateHref, recordModalHost, recordModalHref, } from '../record-modal.js';
|
|
16
16
|
const issuesOf = (source) => {
|
|
17
17
|
const holder = (source ?? {});
|
|
18
18
|
const list = Array.isArray(holder.errors)
|
|
@@ -62,6 +62,8 @@ export const callRecordFunction = async (name, input, options = {}) => {
|
|
|
62
62
|
return { ok: false, issues: issuesOf(value), message: null, status: response.status };
|
|
63
63
|
return { ok: true, value: payload.value };
|
|
64
64
|
};
|
|
65
|
+
/** How many record contexts one island keeps for instant reopening. */
|
|
66
|
+
export const RECORD_MODAL_CACHE_SIZE = 30;
|
|
65
67
|
/**
|
|
66
68
|
* English defaults for every label the runtime itself shows. A module passes its
|
|
67
69
|
* own language through `labels`; these only guarantee a reader never sees a key.
|
|
@@ -74,6 +76,8 @@ export const RECORD_MODAL_LABELS = Object.freeze({
|
|
|
74
76
|
'recordModal.retry': 'Retry',
|
|
75
77
|
'recordModal.errorTitle': 'Not saved',
|
|
76
78
|
'recordModal.saveFailed': 'That did not work. Try again.',
|
|
79
|
+
'recordModal.savedTitle': 'Saved',
|
|
80
|
+
'recordModal.saved': 'The change is in.',
|
|
77
81
|
'recordModal.unsaved': 'Discard what you typed?',
|
|
78
82
|
'recordModal.uploadFailed': 'The file could not be uploaded. Try again.',
|
|
79
83
|
});
|
|
@@ -151,6 +155,56 @@ const inertOutside = (keep) => {
|
|
|
151
155
|
const uuid = () => typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
152
156
|
? crypto.randomUUID()
|
|
153
157
|
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
158
|
+
const emptyDraftState = () => ({ values: {}, checks: {} });
|
|
159
|
+
const draftCheckKey = (name, value) => `${name}\u0000${value}`;
|
|
160
|
+
const after_ = (command) => command.after ?? 'close';
|
|
161
|
+
/**
|
|
162
|
+
* How long a command may run before its button says so. Below this the answer
|
|
163
|
+
* arrives while the reader is still lifting their finger, and a spinner shown
|
|
164
|
+
* and withdrawn inside that window is noise; above it, silence would read as a
|
|
165
|
+
* click that did nothing.
|
|
166
|
+
*/
|
|
167
|
+
export const BUSY_AFTER_MS = 400;
|
|
168
|
+
/**
|
|
169
|
+
* A flag that turns on late and off at once. Work that finishes inside the
|
|
170
|
+
* window never raises it, so a progress indicator bound to it appears only when
|
|
171
|
+
* there is progress to report, and never flashes on its way back out.
|
|
172
|
+
*/
|
|
173
|
+
export const delayedFlag = (show, after = BUSY_AFTER_MS) => {
|
|
174
|
+
let timer;
|
|
175
|
+
const stop = () => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
timer = undefined;
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
set: (running) => {
|
|
181
|
+
stop();
|
|
182
|
+
if (!running) {
|
|
183
|
+
show(false);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
timer = setTimeout(() => show(true), after);
|
|
187
|
+
},
|
|
188
|
+
stop,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/** Controls inside a row do their own thing; the row's destination is for the rest of it. */
|
|
192
|
+
const rowControl = 'a, button, input, select, textarea, label, summary, details, [data-ui="select-cell"]';
|
|
193
|
+
/**
|
|
194
|
+
* Where a click is asking to go: the link it landed on, or the row it landed
|
|
195
|
+
* in. A table row carries its destination on the row itself (`rowLink: false`),
|
|
196
|
+
* so the whole row is one target rather than a link around the first cell. The
|
|
197
|
+
* shell would navigate such a row through the navigation layer, which fetches
|
|
198
|
+
* the collection again and leaves the modal unopened, so this reads it first.
|
|
199
|
+
*/
|
|
200
|
+
export const openerHref = (element) => {
|
|
201
|
+
const anchor = element?.closest('a[href]');
|
|
202
|
+
if (anchor)
|
|
203
|
+
return anchor.target && anchor.target !== '_self' ? null : anchor.href;
|
|
204
|
+
if (element?.closest(rowControl))
|
|
205
|
+
return null;
|
|
206
|
+
return element?.closest('[data-row-href]')?.getAttribute('data-row-href') ?? null;
|
|
207
|
+
};
|
|
154
208
|
/**
|
|
155
209
|
* Create the island controller for one record kind.
|
|
156
210
|
*
|
|
@@ -163,15 +217,45 @@ export const createRecordModal = (definition) => () => {
|
|
|
163
217
|
const envelope = signal(null);
|
|
164
218
|
const failure = signal(null);
|
|
165
219
|
const issues = signal([]);
|
|
166
|
-
const
|
|
220
|
+
const recordDrafts = signal(emptyDraftState());
|
|
221
|
+
const dialogDrafts = signal(emptyDraftState());
|
|
222
|
+
// `running` is the guard — one command at a time — and `busy` is what the
|
|
223
|
+
// views show. They are not the same thing: a save the server answers in
|
|
224
|
+
// twenty milliseconds would otherwise flash the button through its spinner
|
|
225
|
+
// and back, which reads as a glitch rather than as progress. The spinner
|
|
226
|
+
// waits; the guard does not.
|
|
227
|
+
const running = signal(false);
|
|
167
228
|
const busy = signal(false);
|
|
229
|
+
const showBusy = delayedFlag((value) => busy.set(value));
|
|
230
|
+
// A command that succeeded while the modal stayed open. Saving is usually
|
|
231
|
+
// answered before the button could say anything, and a record that looks
|
|
232
|
+
// the same afterwards leaves the reader unsure anything happened, so the
|
|
233
|
+
// form says so where it would have said the opposite.
|
|
234
|
+
const saved = signal(false);
|
|
235
|
+
const setRunning = (value) => {
|
|
236
|
+
running.set(value);
|
|
237
|
+
showBusy.set(value);
|
|
238
|
+
if (value)
|
|
239
|
+
saved.set(false);
|
|
240
|
+
};
|
|
168
241
|
const dialog = signal(null);
|
|
169
242
|
const version = signal(0);
|
|
170
243
|
const viewState = signal({});
|
|
244
|
+
// Contexts this island has read, newest last: reopening a record (or the create
|
|
245
|
+
// form) renders immediately and revalidates in place. Bounded and page-scoped;
|
|
246
|
+
// a successful command or `ket:records-changed` for this kind drops the entries.
|
|
247
|
+
const cache = new Map();
|
|
248
|
+
const remember = (id, value) => {
|
|
249
|
+
cache.delete(id);
|
|
250
|
+
cache.set(id, value);
|
|
251
|
+
while (cache.size > RECORD_MODAL_CACHE_SIZE)
|
|
252
|
+
cache.delete(cache.keys().next().value);
|
|
253
|
+
};
|
|
171
254
|
let root = null;
|
|
172
255
|
let request = null;
|
|
173
256
|
let pushed = false;
|
|
174
257
|
let returnFocus = null;
|
|
258
|
+
let dialogReturnFocus = null;
|
|
175
259
|
let releaseInert = null;
|
|
176
260
|
// Messages of the last record this modal loaded: opening the next record clears
|
|
177
261
|
// the envelope, and its loading state still needs words rather than keys.
|
|
@@ -183,10 +267,12 @@ export const createRecordModal = (definition) => () => {
|
|
|
183
267
|
labels: labels(),
|
|
184
268
|
}), params);
|
|
185
269
|
const visibleTabs = (context) => (definition.tabs ?? []).filter((tab) => tab.visible?.(context) ?? true);
|
|
186
|
-
const contextFor = (current, data) => {
|
|
270
|
+
const contextFor = (current, data, scope = 'record') => {
|
|
271
|
+
const draftState = () => (scope === 'dialog' ? dialogDrafts() : recordDrafts());
|
|
187
272
|
const base = {
|
|
188
273
|
kind: definition.kind,
|
|
189
274
|
id: current.id,
|
|
275
|
+
creating: current.id === RECORD_NEW_ID,
|
|
190
276
|
tab: current.tab,
|
|
191
277
|
data,
|
|
192
278
|
t,
|
|
@@ -194,7 +280,8 @@ export const createRecordModal = (definition) => () => {
|
|
|
194
280
|
const hit = issues().find((issue) => issue.field === name);
|
|
195
281
|
return hit ? (hit.message ?? t(hit.code, hit.params)) : null;
|
|
196
282
|
},
|
|
197
|
-
draft: (name, fallback = '') =>
|
|
283
|
+
draft: (name, fallback = '') => draftState().values[name] ?? fallback,
|
|
284
|
+
draftChecked: (name, value = '1', fallback = false) => draftState().checks[draftCheckKey(name, value)] ?? fallback,
|
|
198
285
|
busy: busy(),
|
|
199
286
|
dialog: dialog(),
|
|
200
287
|
href: (tab) => recordModalHref(location.href, { kind: definition.kind, id: current.id, tab }),
|
|
@@ -209,11 +296,25 @@ export const createRecordModal = (definition) => () => {
|
|
|
209
296
|
request?.abort();
|
|
210
297
|
const controller = new AbortController();
|
|
211
298
|
request = controller;
|
|
299
|
+
// A record opened before shows at once from the in-memory cache and is read
|
|
300
|
+
// again quietly behind it; the reader never waits on a frame they have seen.
|
|
301
|
+
const cached = definition.cache === false ? undefined : cache.get(id);
|
|
302
|
+
if (cached) {
|
|
303
|
+
previousMessages = cached.messages ?? previousMessages;
|
|
304
|
+
envelope.set(cached);
|
|
305
|
+
status.set('ready');
|
|
306
|
+
quiet = true;
|
|
307
|
+
}
|
|
212
308
|
if (!quiet)
|
|
213
309
|
status.set('loading');
|
|
214
310
|
failure.set(null);
|
|
215
311
|
try {
|
|
216
|
-
const
|
|
312
|
+
const creating = id === RECORD_NEW_ID;
|
|
313
|
+
const input = definition.context.input
|
|
314
|
+
? definition.context.input(id, creating)
|
|
315
|
+
: creating
|
|
316
|
+
? {}
|
|
317
|
+
: { id };
|
|
217
318
|
const result = await callRecordFunction(definition.context.fn, input, {
|
|
218
319
|
signal: controller.signal,
|
|
219
320
|
});
|
|
@@ -225,6 +326,8 @@ export const createRecordModal = (definition) => () => {
|
|
|
225
326
|
return;
|
|
226
327
|
}
|
|
227
328
|
previousMessages = result.value.messages ?? previousMessages;
|
|
329
|
+
if (definition.cache !== false)
|
|
330
|
+
remember(id, result.value);
|
|
228
331
|
envelope.set(result.value);
|
|
229
332
|
status.set('ready');
|
|
230
333
|
}
|
|
@@ -235,30 +338,64 @@ export const createRecordModal = (definition) => () => {
|
|
|
235
338
|
status.set('error');
|
|
236
339
|
}
|
|
237
340
|
};
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
341
|
+
const layers = () => root ? [...root.querySelectorAll('[data-ui="modal-layer"][data-client-modal="true"]')] : [];
|
|
342
|
+
const recordLayer = () => layers()[0] ?? null;
|
|
343
|
+
const topLayer = () => layers().at(-1) ?? null;
|
|
344
|
+
const keepDrafts = (current, scope) => {
|
|
345
|
+
if (!current)
|
|
242
346
|
return;
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
347
|
+
const previous = scope === 'dialog' ? dialogDrafts() : recordDrafts();
|
|
348
|
+
const kept = {
|
|
349
|
+
values: { ...previous.values },
|
|
350
|
+
checks: { ...previous.checks },
|
|
351
|
+
};
|
|
352
|
+
for (const control of current.querySelectorAll('input[name], select[name], textarea[name]')) {
|
|
353
|
+
if (control.disabled || control.name === RECORD_COMMAND_FIELD)
|
|
354
|
+
continue;
|
|
355
|
+
if (control instanceof HTMLInputElement) {
|
|
356
|
+
if (control.type === 'file' || control.type === 'hidden')
|
|
357
|
+
continue;
|
|
358
|
+
if (control.type === 'checkbox' || control.type === 'radio') {
|
|
359
|
+
kept.checks[draftCheckKey(control.name, control.value)] = control.checked;
|
|
360
|
+
if (control.type === 'checkbox')
|
|
361
|
+
kept.values[control.name] = control.checked ? control.value : '';
|
|
362
|
+
else if (control.checked)
|
|
363
|
+
kept.values[control.name] = control.value;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
kept.values[control.name] = control.value;
|
|
368
|
+
}
|
|
369
|
+
if (scope === 'dialog')
|
|
370
|
+
dialogDrafts.set(kept);
|
|
371
|
+
else
|
|
372
|
+
recordDrafts.set(kept);
|
|
373
|
+
};
|
|
374
|
+
const keepAllDrafts = () => {
|
|
375
|
+
const currentLayers = layers();
|
|
376
|
+
keepDrafts(currentLayers[0] ?? null, 'record');
|
|
377
|
+
if (currentLayers.length > 1)
|
|
378
|
+
keepDrafts(currentLayers.at(-1) ?? null, 'dialog');
|
|
249
379
|
};
|
|
250
|
-
const mayDiscard = () => {
|
|
251
|
-
const current = layer();
|
|
380
|
+
const mayDiscard = (current) => {
|
|
252
381
|
if (!current || !recordLayerHasDraft(current))
|
|
253
382
|
return true;
|
|
254
383
|
return globalThis.confirm(t('recordModal.unsaved'));
|
|
255
384
|
};
|
|
256
|
-
const afterRender = () => {
|
|
385
|
+
const afterRender = (preferred) => {
|
|
257
386
|
requestAnimationFrame(() => {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
387
|
+
const currentLayers = layers();
|
|
388
|
+
const record = currentLayers[0];
|
|
389
|
+
const top = currentLayers.at(-1);
|
|
260
390
|
if (!top)
|
|
261
391
|
return;
|
|
392
|
+
if (record)
|
|
393
|
+
record.inert = currentLayers.length > 1;
|
|
394
|
+
const target = preferred?.();
|
|
395
|
+
if (target?.isConnected && top.contains(target)) {
|
|
396
|
+
target.focus();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
262
399
|
if (top.contains(document.activeElement))
|
|
263
400
|
return;
|
|
264
401
|
const sheet = top.querySelector('[data-ui="modal-sheet"]');
|
|
@@ -266,22 +403,30 @@ export const createRecordModal = (definition) => () => {
|
|
|
266
403
|
(first ?? sheet)?.focus();
|
|
267
404
|
});
|
|
268
405
|
};
|
|
269
|
-
const show = (id, tab, how) => {
|
|
406
|
+
const show = (id, tab, how, preferredFocus) => {
|
|
270
407
|
const current = open();
|
|
271
408
|
const sameRecord = current?.id === id;
|
|
272
409
|
const nextTab = tab ?? (sameRecord ? current.tab : '');
|
|
273
410
|
if (!sameRecord) {
|
|
274
411
|
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
275
412
|
issues.set([]);
|
|
276
|
-
|
|
413
|
+
saved.set(false);
|
|
414
|
+
recordDrafts.set(emptyDraftState());
|
|
415
|
+
dialogDrafts.set(emptyDraftState());
|
|
277
416
|
viewState.set({});
|
|
278
417
|
dialog.set(null);
|
|
418
|
+
dialogReturnFocus = null;
|
|
279
419
|
envelope.set(null);
|
|
280
420
|
void load(id);
|
|
281
421
|
}
|
|
282
422
|
open.set({ id, tab: nextTab });
|
|
283
423
|
const href = recordModalHref(location.href, { kind: definition.kind, id, tab: nextTab || null });
|
|
284
424
|
if (how === 'push') {
|
|
425
|
+
// The shell's own navigation snapshots scroll onto the entry it leaves
|
|
426
|
+
// before pushing (`saveScroll` in packages/ketjs/src/server/http.ts) so
|
|
427
|
+
// going back restores it; this push must do the same, or closing the
|
|
428
|
+
// modal later restores no scroll and the page it sits over jumps to top.
|
|
429
|
+
history.replaceState({ ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] }, '', location.href);
|
|
285
430
|
history.pushState({ ...(history.state ?? {}), __ketRecordModal: definition.kind }, '', href);
|
|
286
431
|
pushed = true;
|
|
287
432
|
}
|
|
@@ -289,14 +434,16 @@ export const createRecordModal = (definition) => () => {
|
|
|
289
434
|
history.replaceState(history.state ?? {}, '', href);
|
|
290
435
|
if (root && !releaseInert)
|
|
291
436
|
releaseInert = inertOutside(root);
|
|
292
|
-
afterRender();
|
|
437
|
+
afterRender(preferredFocus);
|
|
293
438
|
};
|
|
294
439
|
const hide = (how) => {
|
|
295
440
|
request?.abort();
|
|
296
441
|
open.set(null);
|
|
297
442
|
dialog.set(null);
|
|
298
443
|
issues.set([]);
|
|
299
|
-
|
|
444
|
+
saved.set(false);
|
|
445
|
+
recordDrafts.set(emptyDraftState());
|
|
446
|
+
dialogDrafts.set(emptyDraftState());
|
|
300
447
|
viewState.set({});
|
|
301
448
|
status.set('idle');
|
|
302
449
|
envelope.set(null);
|
|
@@ -312,19 +459,23 @@ export const createRecordModal = (definition) => () => {
|
|
|
312
459
|
}
|
|
313
460
|
const target = returnFocus;
|
|
314
461
|
returnFocus = null;
|
|
462
|
+
dialogReturnFocus = null;
|
|
315
463
|
if (target?.isConnected)
|
|
316
464
|
requestAnimationFrame(() => target.focus());
|
|
317
465
|
};
|
|
318
466
|
const close = () => {
|
|
319
467
|
if (dialog()) {
|
|
320
|
-
if (!mayDiscard())
|
|
468
|
+
if (!mayDiscard(topLayer()))
|
|
321
469
|
return;
|
|
470
|
+
const target = dialogReturnFocus;
|
|
471
|
+
dialogReturnFocus = null;
|
|
322
472
|
dialog.set(null);
|
|
473
|
+
dialogDrafts.set(emptyDraftState());
|
|
323
474
|
issues.set([]);
|
|
324
|
-
afterRender();
|
|
475
|
+
afterRender(() => (target?.isConnected ? target : null));
|
|
325
476
|
return;
|
|
326
477
|
}
|
|
327
|
-
if (!mayDiscard())
|
|
478
|
+
if (!mayDiscard(recordLayer()))
|
|
328
479
|
return;
|
|
329
480
|
hide('history');
|
|
330
481
|
};
|
|
@@ -332,11 +483,19 @@ export const createRecordModal = (definition) => () => {
|
|
|
332
483
|
const command = definition.commands?.[name];
|
|
333
484
|
const current = open();
|
|
334
485
|
const data = envelope()?.data;
|
|
335
|
-
if (!command || !current || data === undefined ||
|
|
486
|
+
if (!command || !current || data === undefined || running())
|
|
336
487
|
return;
|
|
337
488
|
const formData = new FormData(form, submitter instanceof HTMLButtonElement ? submitter : null);
|
|
338
|
-
const
|
|
339
|
-
|
|
489
|
+
const currentLayer = form.closest('[data-ui="modal-layer"][data-client-modal="true"]');
|
|
490
|
+
const scope = dialog() && currentLayer === topLayer() ? 'dialog' : 'record';
|
|
491
|
+
keepDrafts(currentLayer, scope);
|
|
492
|
+
const context = contextFor(current, data, scope);
|
|
493
|
+
if (command.confirm) {
|
|
494
|
+
const message = command.confirm(context);
|
|
495
|
+
if (message && !globalThis.confirm(message))
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
setRunning(true);
|
|
340
499
|
issues.set([]);
|
|
341
500
|
try {
|
|
342
501
|
const uploads = {};
|
|
@@ -366,16 +525,20 @@ export const createRecordModal = (definition) => () => {
|
|
|
366
525
|
name: typeof stored.name === 'string' ? stored.name : null,
|
|
367
526
|
};
|
|
368
527
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
528
|
+
const invoke = (fn, input) => callRecordFunction(fn, input, { idempotencyKey: uuid() });
|
|
529
|
+
let result = await invoke(command.fn, command.input(formData, context, uploads));
|
|
530
|
+
// Further calls run only once the first succeeds, and stop at the first
|
|
531
|
+
// one that does not — a later step's success never papers over an
|
|
532
|
+
// earlier failure.
|
|
533
|
+
for (const step of result.ok ? (command.also ?? []) : []) {
|
|
534
|
+
if (step.when && !step.when(context))
|
|
535
|
+
continue;
|
|
536
|
+
result = await invoke(step.fn, step.input(formData, context, uploads));
|
|
537
|
+
if (!result.ok)
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
372
540
|
if (!result.ok) {
|
|
373
|
-
//
|
|
374
|
-
const kept = {};
|
|
375
|
-
for (const [key, value] of formData)
|
|
376
|
-
if (typeof value === 'string')
|
|
377
|
-
kept[key] = value;
|
|
378
|
-
drafts.set({ ...drafts(), ...kept });
|
|
541
|
+
// The layer was snapshotted before the busy state rendered, including unchecked controls.
|
|
379
542
|
issues.set(result.issues.length
|
|
380
543
|
? result.issues
|
|
381
544
|
: [
|
|
@@ -388,35 +551,77 @@ export const createRecordModal = (definition) => () => {
|
|
|
388
551
|
]);
|
|
389
552
|
return;
|
|
390
553
|
}
|
|
391
|
-
|
|
554
|
+
if (scope === 'dialog')
|
|
555
|
+
dialogDrafts.set(emptyDraftState());
|
|
556
|
+
else
|
|
557
|
+
recordDrafts.set(emptyDraftState());
|
|
392
558
|
form.reset();
|
|
393
|
-
|
|
394
|
-
const
|
|
559
|
+
const value = result.value;
|
|
560
|
+
const createdId = after_(command) === 'open'
|
|
561
|
+
? (command.created?.(result.value) ??
|
|
562
|
+
(value && typeof value === 'object' && value.id != null ? String(value.id) : null))
|
|
563
|
+
: null;
|
|
564
|
+
// What the command changed is no longer what the cache holds.
|
|
565
|
+
cache.delete(current.id);
|
|
566
|
+
if (createdId)
|
|
567
|
+
cache.delete(createdId);
|
|
568
|
+
const announce = () => {
|
|
569
|
+
document.dispatchEvent(new CustomEvent('ket:records-changed', {
|
|
570
|
+
detail: { kind: definition.kind, ids: [createdId ?? current.id] },
|
|
571
|
+
}));
|
|
572
|
+
};
|
|
573
|
+
const after = after_(command);
|
|
574
|
+
// A modal that closes says so by closing; one that stays owes an answer.
|
|
575
|
+
if (after !== 'close')
|
|
576
|
+
saved.set(true);
|
|
577
|
+
if (after === 'open') {
|
|
578
|
+
dialog.set(null);
|
|
579
|
+
dialogDrafts.set(emptyDraftState());
|
|
580
|
+
// Replace `:new` in the address bar before the collection refreshes: the
|
|
581
|
+
// shell re-fetches `location.href`, which must already name the new record.
|
|
582
|
+
if (createdId)
|
|
583
|
+
show(createdId, command.openTab ?? null, 'replace');
|
|
584
|
+
else
|
|
585
|
+
hide('history');
|
|
586
|
+
announce();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
announce();
|
|
395
590
|
if (after === 'close')
|
|
396
591
|
hide('history');
|
|
397
592
|
else if (after === 'reload') {
|
|
593
|
+
const target = dialogReturnFocus;
|
|
594
|
+
dialogReturnFocus = null;
|
|
398
595
|
dialog.set(null);
|
|
596
|
+
dialogDrafts.set(emptyDraftState());
|
|
399
597
|
await load(current.id);
|
|
598
|
+
afterRender(() => (target?.isConnected ? target : null));
|
|
400
599
|
}
|
|
401
600
|
else if (after === 'refresh')
|
|
402
601
|
await load(current.id, true);
|
|
403
602
|
else if (after === 'stay')
|
|
404
603
|
version.set(version() + 1);
|
|
405
604
|
else if ('tab' in after) {
|
|
605
|
+
dialogReturnFocus = null;
|
|
406
606
|
dialog.set(null);
|
|
607
|
+
dialogDrafts.set(emptyDraftState());
|
|
407
608
|
show(current.id, after.tab, 'replace');
|
|
408
609
|
await load(current.id);
|
|
409
610
|
}
|
|
410
611
|
else {
|
|
612
|
+
const target = after.dialog ? null : dialogReturnFocus;
|
|
613
|
+
dialogReturnFocus = after.dialog && submitter instanceof HTMLElement ? submitter : null;
|
|
411
614
|
dialog.set(after.dialog ? { name: after.dialog, params: {} } : null);
|
|
615
|
+
dialogDrafts.set(emptyDraftState());
|
|
412
616
|
await load(current.id, true);
|
|
617
|
+
afterRender(() => (target?.isConnected ? target : null));
|
|
413
618
|
}
|
|
414
619
|
}
|
|
415
620
|
catch {
|
|
416
621
|
issues.set([{ field: null, code: 'recordModal.saveFailed', message: null, params: {} }]);
|
|
417
622
|
}
|
|
418
623
|
finally {
|
|
419
|
-
|
|
624
|
+
setRunning(false);
|
|
420
625
|
}
|
|
421
626
|
};
|
|
422
627
|
const formIssues = (context) => {
|
|
@@ -424,7 +629,13 @@ export const createRecordModal = (definition) => () => {
|
|
|
424
629
|
const fieldless = issues().filter((issue) => issue.field && !root?.querySelector(`[name="${CSS.escape(issue.field)}"]`));
|
|
425
630
|
const all = [...general, ...fieldless];
|
|
426
631
|
if (!all.length)
|
|
427
|
-
return
|
|
632
|
+
return saved()
|
|
633
|
+
? Notice({
|
|
634
|
+
title: context.t('recordModal.savedTitle'),
|
|
635
|
+
message: context.t('recordModal.saved'),
|
|
636
|
+
tone: 'positive',
|
|
637
|
+
})
|
|
638
|
+
: '';
|
|
428
639
|
return Notice({
|
|
429
640
|
title: context.t('recordModal.errorTitle'),
|
|
430
641
|
message: all.map((issue) => issue.message ?? context.t(issue.code, issue.params)).join(' · '),
|
|
@@ -447,17 +658,23 @@ export const createRecordModal = (definition) => () => {
|
|
|
447
658
|
const context = contextFor(current, data);
|
|
448
659
|
const tabs = visibleTabs(context);
|
|
449
660
|
const active = tabs.find((tab) => tab.id === context.tab);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
:
|
|
661
|
+
const header = definition.header?.(context) ?? '';
|
|
662
|
+
const issueNotice = formIssues(context);
|
|
663
|
+
const body = active ? active.view(context) : (definition.body?.(context) ?? '');
|
|
664
|
+
if (tabs.length)
|
|
665
|
+
return TabbedView({
|
|
666
|
+
id: `record-view-${definition.kind.replaceAll('.', '-')}`,
|
|
667
|
+
label: definition.title(context),
|
|
668
|
+
items: tabs.map((tab) => ({
|
|
669
|
+
id: tab.id,
|
|
670
|
+
label: tab.label(context),
|
|
671
|
+
href: context.href(tab.id),
|
|
672
|
+
active: tab.id === context.tab,
|
|
673
|
+
})),
|
|
674
|
+
context: header !== '' || issueNotice !== '' ? (_jsxs(_Fragment, { children: [header, issueNotice] })) : undefined,
|
|
675
|
+
body,
|
|
676
|
+
});
|
|
677
|
+
return (_jsxs(_Fragment, { children: [header, issueNotice, body] }));
|
|
461
678
|
};
|
|
462
679
|
const dialogLayer = () => {
|
|
463
680
|
const current = open();
|
|
@@ -468,7 +685,7 @@ export const createRecordModal = (definition) => () => {
|
|
|
468
685
|
const spec = definition.dialogs?.[opened.name];
|
|
469
686
|
if (!spec)
|
|
470
687
|
return '';
|
|
471
|
-
const context = contextFor(current, data);
|
|
688
|
+
const context = contextFor(current, data, 'dialog');
|
|
472
689
|
return ModalSheet({
|
|
473
690
|
id: `record-dialog-${definition.kind.replaceAll('.', '-')}-${opened.name}`,
|
|
474
691
|
mode: 'client',
|
|
@@ -492,8 +709,14 @@ export const createRecordModal = (definition) => () => {
|
|
|
492
709
|
mode: 'client',
|
|
493
710
|
presentation: 'dialog',
|
|
494
711
|
size: definition.size ?? 'default',
|
|
712
|
+
// Tabs have different heights; a fixed dialog does not jump when the reader switches
|
|
713
|
+
// tabs, nor when the loading state gives way to the record.
|
|
714
|
+
height: (definition.tabs?.length ?? 0) > 1 ? 'fixed' : 'content',
|
|
715
|
+
fixedHeight: definition.fixedHeight,
|
|
495
716
|
title: context ? definition.title(context) : t('recordModal.loading'),
|
|
496
717
|
description: context ? (definition.description?.(context) ?? null) : null,
|
|
718
|
+
status: context ? definition.status?.(context) : undefined,
|
|
719
|
+
actions: context ? definition.actions?.(context) : undefined,
|
|
497
720
|
closeLabel: t('recordModal.close'),
|
|
498
721
|
body: recordBody(),
|
|
499
722
|
}), dialogLayer()] }));
|
|
@@ -513,7 +736,11 @@ export const createRecordModal = (definition) => () => {
|
|
|
513
736
|
const element = event.target instanceof Element ? event.target : null;
|
|
514
737
|
const inModal = element?.closest('[data-ui="modal-layer"][data-client-modal="true"]');
|
|
515
738
|
if (inModal && root?.contains(inModal)) {
|
|
516
|
-
|
|
739
|
+
// `data-ui="modal-close"` is the chrome's own × control; `data-record-close`
|
|
740
|
+
// marks a module's own labeled close button placed elsewhere in the body
|
|
741
|
+
// (the two can't share one attribute — a labeled button needs `data-ui="action"`
|
|
742
|
+
// for its styling, not the icon-only close control's).
|
|
743
|
+
if (element?.closest('[data-ui="modal-close"], [data-ui="modal-backdrop"], [data-record-close]')) {
|
|
517
744
|
event.preventDefault();
|
|
518
745
|
close();
|
|
519
746
|
return;
|
|
@@ -521,6 +748,7 @@ export const createRecordModal = (definition) => () => {
|
|
|
521
748
|
const stateControl = element?.closest('button[data-record-state], a[data-record-state]');
|
|
522
749
|
if (stateControl) {
|
|
523
750
|
event.preventDefault();
|
|
751
|
+
keepAllDrafts();
|
|
524
752
|
viewState.set({
|
|
525
753
|
...viewState(),
|
|
526
754
|
[stateControl.getAttribute('data-record-state') ?? '']: stateControl.getAttribute('data-record-value') ?? '',
|
|
@@ -530,6 +758,12 @@ export const createRecordModal = (definition) => () => {
|
|
|
530
758
|
const opener = element?.closest(`[${RECORD_DIALOG_ATTRIBUTE}]`);
|
|
531
759
|
if (opener) {
|
|
532
760
|
event.preventDefault();
|
|
761
|
+
const focused = document.activeElement;
|
|
762
|
+
dialogReturnFocus =
|
|
763
|
+
focused instanceof HTMLElement && opener.contains(focused)
|
|
764
|
+
? focused
|
|
765
|
+
: opener.querySelector(focusable);
|
|
766
|
+
dialogDrafts.set(emptyDraftState());
|
|
533
767
|
const params = {};
|
|
534
768
|
for (const [key, value] of Object.entries(opener.dataset))
|
|
535
769
|
if (key.startsWith('recordParam') && value !== undefined)
|
|
@@ -547,10 +781,10 @@ export const createRecordModal = (definition) => () => {
|
|
|
547
781
|
return;
|
|
548
782
|
}
|
|
549
783
|
}
|
|
550
|
-
const
|
|
551
|
-
if (!
|
|
784
|
+
const href = openerHref(element);
|
|
785
|
+
if (!href)
|
|
552
786
|
return;
|
|
553
|
-
const url = new URL(
|
|
787
|
+
const url = new URL(href, location.href);
|
|
554
788
|
if (url.origin !== location.origin || url.pathname !== location.pathname)
|
|
555
789
|
return;
|
|
556
790
|
const target = readRecordModalTarget(url);
|
|
@@ -563,9 +797,9 @@ export const createRecordModal = (definition) => () => {
|
|
|
563
797
|
return;
|
|
564
798
|
// Switching tab is not discarding: what was typed rides along as drafts,
|
|
565
799
|
// which the views read back, so no prompt stands between two tabs.
|
|
566
|
-
keepDrafts();
|
|
800
|
+
keepDrafts(recordLayer(), 'record');
|
|
567
801
|
issues.set([]);
|
|
568
|
-
show(target.id, target.tab ?? null, 'replace');
|
|
802
|
+
show(target.id, target.tab ?? null, 'replace', () => recordLayer()?.querySelector('[data-ui="tab"][data-active="true"]') ?? null);
|
|
569
803
|
return;
|
|
570
804
|
}
|
|
571
805
|
show(target.id, target.tab ?? null, 'push');
|
|
@@ -573,6 +807,24 @@ export const createRecordModal = (definition) => () => {
|
|
|
573
807
|
// Capture: the navigation layer listens on the document too, and was
|
|
574
808
|
// installed first, so a bubbling listener would see the page fetched.
|
|
575
809
|
{ signal: lifetime, capture: true });
|
|
810
|
+
// A row is reached by keyboard, not by pointer alone: Enter and Space on
|
|
811
|
+
// the focused row open what clicking it opens.
|
|
812
|
+
document.addEventListener('keydown', (event) => {
|
|
813
|
+
if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' '))
|
|
814
|
+
return;
|
|
815
|
+
const element = event.target instanceof Element ? event.target : null;
|
|
816
|
+
if (!element?.matches('[data-row-href][tabindex="0"]'))
|
|
817
|
+
return;
|
|
818
|
+
const href = element.getAttribute('data-row-href');
|
|
819
|
+
const url = href ? new URL(href, location.href) : null;
|
|
820
|
+
if (!url || url.origin !== location.origin || url.pathname !== location.pathname)
|
|
821
|
+
return;
|
|
822
|
+
const target = readRecordModalTarget(url);
|
|
823
|
+
if (!target || target.kind !== definition.kind)
|
|
824
|
+
return;
|
|
825
|
+
event.preventDefault();
|
|
826
|
+
show(target.id, target.tab ?? null, 'push');
|
|
827
|
+
}, { signal: lifetime, capture: true });
|
|
576
828
|
document.addEventListener('submit', (event) => {
|
|
577
829
|
const form = event.target instanceof HTMLFormElement ? event.target : null;
|
|
578
830
|
if (!form || !root?.contains(form))
|
|
@@ -625,6 +877,7 @@ export const createRecordModal = (definition) => () => {
|
|
|
625
877
|
return;
|
|
626
878
|
if ((control instanceof HTMLSelectElement || control instanceof HTMLInputElement) &&
|
|
627
879
|
control.hasAttribute('data-record-state')) {
|
|
880
|
+
keepAllDrafts();
|
|
628
881
|
viewState.set({
|
|
629
882
|
...viewState(),
|
|
630
883
|
[control.getAttribute('data-record-state') ?? '']: control.value,
|
|
@@ -692,6 +945,18 @@ export const createRecordModal = (definition) => () => {
|
|
|
692
945
|
});
|
|
693
946
|
observer.observe(root, { childList: true, subtree: true });
|
|
694
947
|
lifetime.addEventListener('abort', () => observer.disconnect());
|
|
948
|
+
// Another island (or this one) changed records of this kind: drop them so the
|
|
949
|
+
// next opening reads fresh data first.
|
|
950
|
+
document.addEventListener('ket:records-changed', (event) => {
|
|
951
|
+
const detail = event.detail;
|
|
952
|
+
if (detail?.kind !== definition.kind)
|
|
953
|
+
return;
|
|
954
|
+
if (!Array.isArray(detail.ids) || !detail.ids.length)
|
|
955
|
+
cache.clear();
|
|
956
|
+
else
|
|
957
|
+
for (const id of detail.ids)
|
|
958
|
+
cache.delete(String(id));
|
|
959
|
+
}, { signal: lifetime });
|
|
695
960
|
// Back and forward over entries this modal created change only the modal.
|
|
696
961
|
document.addEventListener('ket:popstate', (event) => {
|
|
697
962
|
const target = readRecordModalTarget(location.href);
|
|
@@ -709,6 +974,7 @@ export const createRecordModal = (definition) => () => {
|
|
|
709
974
|
show(initial.id, initial.tab ?? null, 'none');
|
|
710
975
|
lifetime.addEventListener('abort', () => {
|
|
711
976
|
request?.abort();
|
|
977
|
+
showBusy.stop();
|
|
712
978
|
releaseInert?.();
|
|
713
979
|
releaseInert = null;
|
|
714
980
|
root = null;
|
|
@@ -721,5 +987,5 @@ export const createRecordModal = (definition) => () => {
|
|
|
721
987
|
* appears and disposes it when the host leaves; props must be JSON.
|
|
722
988
|
*/
|
|
723
989
|
export const recordIsland = (name, props) => (_jsx("ket-island", { "data-island": name, "data-props": JSON.stringify(props) }));
|
|
724
|
-
export { RECORD_PARAM, RECORD_TAB_PARAM, recordModalHref, recordModalClosedHref, readRecordModalTarget };
|
|
990
|
+
export { RECORD_NEW_ID, RECORD_PARAM, RECORD_TAB_PARAM, recordModalHref, recordModalCreateHref, recordModalClosedHref, readRecordModalTarget, };
|
|
725
991
|
//# sourceMappingURL=record-modal.js.map
|