@zeyos/zx 4.4.0 → 5.2.0
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/CHANGELOG.md +447 -0
- package/README.md +44 -11
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +31 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +24 -9
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +54 -2
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +26 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +268 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
- package/dist/types/components/menu-button/menu-button.d.ts +39 -5
- package/dist/types/components/message/message.d.ts +132 -17
- package/dist/types/components/modal/modal.d.ts +43 -2
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +17 -3
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +311 -18
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +42 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +110 -5
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +345 -22
- package/dist/types/zeyos/view.d.ts +26 -2
- package/dist/zx-zeyos.esm.js +6178 -1008
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +2861 -827
- package/dist/zx.esm.js +8909 -1480
- package/dist/zx.esm.min.js +55 -7
- package/dist/zx.global.js +8911 -1482
- package/dist/zx.global.min.js +55 -7
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1079 -117
- package/docs/llms.md +956 -154
- package/docs/llms.txt +9 -5
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +61 -1
- package/src/components/app-icon/app-icon.css +14 -3
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +59 -79
- package/src/components/app-sidebar/app-sidebar.css +304 -168
- package/src/components/app-sidebar/app-sidebar.js +667 -46
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +47 -47
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +4 -4
- package/src/components/card-view/card-view.css +14 -12
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +7 -6
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +9 -4
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +3 -3
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +159 -72
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +101 -29
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +38 -64
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +307 -11
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +16 -14
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +42 -18
- package/src/components/kanban-view/kanban-view.js +9 -6
- package/src/components/launcher/launcher.css +108 -26
- package/src/components/launcher/launcher.js +530 -42
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +40 -14
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +72 -4
- package/src/components/menu-button/menu-button.js +160 -18
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +264 -22
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +83 -15
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +3 -1
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +13 -11
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +4 -3
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +8 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +92 -47
- package/src/components/sheet/sheet.css +12 -9
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +18 -4
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +42 -47
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +41 -32
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +187 -37
- package/src/components/table/table.js +718 -73
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +44 -19
- package/src/components/tree/tree.css +26 -17
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/breakpoint.js +1 -1
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +204 -61
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +77 -3
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +10 -5
- package/src/internal/app-rail.js +135 -55
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +69 -27
- package/src/zeyos/index.js +4 -1
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +197 -13
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +287 -84
- package/src/zeyos/view.js +49 -93
- package/styles/base.css +72 -1
- package/styles/icons.css +11 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +117 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +140 -43
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +3 -1
- package/styles/tokens/density.css +0 -9
package/src/internal/app-rail.js
CHANGED
|
@@ -17,6 +17,9 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
|
|
|
17
17
|
* @property {() => void} [invoke] Application-owned non-link action.
|
|
18
18
|
* @property {boolean} [disabled] Whether activation is unavailable.
|
|
19
19
|
* @property {AppNavItem[]} [children] Child destinations/actions.
|
|
20
|
+
* @property {boolean} [section] Makes the item a section: a quiet heading over its `children`, which
|
|
21
|
+
* render at the same level. A section is never focused, selected, or collapsed, and needs no `id`.
|
|
22
|
+
* @property {Node|{toElement: () => Node|null}} [actions] A section's trailing controls, such as an add button.
|
|
20
23
|
*/
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {Object} AppRailPresenterOptions
|
|
@@ -45,6 +48,14 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
|
|
|
45
48
|
export class AppRailPresenter extends Component {
|
|
46
49
|
static cssName = 'app-rail';
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* BEM block of the rail's own parts (header, list, footer, row, item). A component built on the
|
|
53
|
+
* presenter names its parts after itself; flyout panels keep the `zx-app-rail__` vocabulary,
|
|
54
|
+
* because every rail portals the same panel.
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
static partBlock = 'zx-app-rail';
|
|
58
|
+
|
|
48
59
|
/** @type {Readonly<AppRailPresenterOptions>} */
|
|
49
60
|
static defaults = {
|
|
50
61
|
items: [],
|
|
@@ -93,13 +104,14 @@ export class AppRailPresenter extends Component {
|
|
|
93
104
|
root.dataset.orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
94
105
|
const sides = root.dataset.orientation === 'horizontal' ? ['top', 'bottom'] : ['left', 'right'];
|
|
95
106
|
root.dataset.side = sides.includes(this.options.side) ? this.options.side : sides[0];
|
|
96
|
-
const
|
|
97
|
-
const
|
|
107
|
+
const block = this._partBlock();
|
|
108
|
+
const header = h('div', { class: `${block}__header` });
|
|
109
|
+
const footer = h('div', { class: `${block}__footer` });
|
|
98
110
|
appendSlot(header, this.options.header);
|
|
99
111
|
appendSlot(footer, this.options.footer);
|
|
100
112
|
header.hidden = !header.hasChildNodes();
|
|
101
113
|
footer.hidden = !footer.hasChildNodes();
|
|
102
|
-
root.replaceChildren(header, h('ul', { ref: 'list', class:
|
|
114
|
+
root.replaceChildren(header, h('ul', { ref: 'list', class: `${block}__list` }), footer);
|
|
103
115
|
this.listen(root, 'keydown', (event) => this._onRailKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
104
116
|
return root;
|
|
105
117
|
}
|
|
@@ -209,31 +221,28 @@ export class AppRailPresenter extends Component {
|
|
|
209
221
|
this._clearArtifacts();
|
|
210
222
|
this.refs.list.replaceChildren();
|
|
211
223
|
this._interaction = new RailListenerScope(/** @type {HTMLElement} */ (this.el));
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
level: 1,
|
|
225
|
-
location: 'root'
|
|
226
|
-
})
|
|
227
|
-
});
|
|
228
|
-
const row = h('li', { class: 'zx-app-rail__row' }, control);
|
|
224
|
+
let started = false;
|
|
225
|
+
for (const item of railEntries(this._items)) {
|
|
226
|
+
// A section has no rail control: the rail draws a rule between the groups instead.
|
|
227
|
+
if (item === SECTION_BREAK) {
|
|
228
|
+
if (started) this.refs.list.append(h('li', { class: `${this._partBlock()}__separator`, ariaHidden: 'true' }));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
started = true;
|
|
232
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0
|
|
233
|
+
&& this._flyoutsFor(item);
|
|
234
|
+
const control = this._renderControl(item, hasChildren);
|
|
235
|
+
const row = h('li', { class: `${this._partBlock()}__row` }, control);
|
|
229
236
|
this.refs.list.append(row);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
if (this._tooltipFor(item)) {
|
|
238
|
+
const tooltip = new Tooltip(control, {
|
|
239
|
+
content: item.label,
|
|
240
|
+
placement: tooltipPlacement(this.el.dataset.side),
|
|
241
|
+
delay: 450
|
|
242
|
+
});
|
|
243
|
+
this._tooltips.push(tooltip);
|
|
244
|
+
this._tooltipById.set(String(item.id), tooltip);
|
|
245
|
+
}
|
|
237
246
|
if (hasChildren) this._buildFlyout(item, control, String(item.id), null);
|
|
238
247
|
else this._interaction.listen(control, 'click', (event) => {
|
|
239
248
|
const selected = this._select(item, event);
|
|
@@ -243,21 +252,66 @@ export class AppRailPresenter extends Component {
|
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
254
|
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (open.some((flyout) => path.includes(flyout.panel) || path.includes(flyout.trigger))) return;
|
|
252
|
-
for (const flyout of open) if (flyout.parentKey === null) this.closeFlyout(flyout.key);
|
|
253
|
-
});
|
|
255
|
+
}
|
|
256
|
+
// Outside presses need no listener here: each open flyout is a Dropdown on the overlay stack,
|
|
257
|
+
// which closes it on a press outside it and every flyout opened from it, and a nested flyout,
|
|
258
|
+
// anchored inside its parent's panel, counts as part of that parent.
|
|
259
|
+
this._afterRebuild();
|
|
254
260
|
this._syncActive();
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
/** @returns {string} The BEM block of the rail's own parts. @protected */
|
|
264
|
+
_partBlock() {
|
|
265
|
+
return /** @type {typeof AppRailPresenter} */ (this.constructor).partBlock;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Builds one top-level rail control. The control must carry `data-app-nav-id` and
|
|
270
|
+
* `data-app-nav-value-id`, which keyboard handling, flyouts, and the active state read.
|
|
271
|
+
* @param {AppNavItem} item Top-level item.
|
|
272
|
+
* @param {boolean} hasFlyout Whether the control opens a flyout of the item's children.
|
|
273
|
+
* @returns {HTMLElement}
|
|
274
|
+
* @protected
|
|
275
|
+
*/
|
|
276
|
+
_renderControl(item, hasFlyout) {
|
|
277
|
+
return navControl(item, {
|
|
278
|
+
className: 'zx-app-rail__item',
|
|
279
|
+
index: String(item.id),
|
|
280
|
+
iconOnly: true,
|
|
281
|
+
disclosure: hasFlyout,
|
|
282
|
+
disclosureIcon: railDisclosureIcon(this.el.dataset.side),
|
|
283
|
+
visual: appNavIcon(item, this.options.renderIcon, {
|
|
284
|
+
orientation: this.el.dataset.orientation,
|
|
285
|
+
collapsed: true,
|
|
286
|
+
side: this.el.dataset.side,
|
|
287
|
+
level: 1,
|
|
288
|
+
location: 'root'
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Whether an item with children presents them in a flyout. @param {AppNavItem} item @returns {boolean} @protected */
|
|
294
|
+
_flyoutsFor(item) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Whether a top-level control gets a hover/focus tooltip naming it. @param {AppNavItem} item @returns {boolean} @protected */
|
|
299
|
+
_tooltipFor(item) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Runs after every rebuild, before the active state is applied. @returns {void} @protected */
|
|
304
|
+
_afterRebuild() {}
|
|
305
|
+
|
|
257
306
|
/** @param {AppNavItem} item @param {HTMLElement} trigger @param {string} key @param {string|null} parentKey @returns {void} */
|
|
258
307
|
_buildFlyout(item, trigger, key, parentKey) {
|
|
259
308
|
const list = h('ul', { class: 'zx-app-rail__flyout-list' });
|
|
260
|
-
for (const child of item.children ?? []) {
|
|
309
|
+
for (const child of railEntries(item.children ?? [], true)) {
|
|
310
|
+
if (child.section) {
|
|
311
|
+
// A caption, not a destination: keyboard movement skips it with every other non-action.
|
|
312
|
+
list.append(h('li', { class: 'zx-app-rail__flyout-section' }, child.label));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
261
315
|
const childKey = `${key}/${child.id}`;
|
|
262
316
|
const hasChildren = Boolean(child.children?.length);
|
|
263
317
|
const control = navControl(child, {
|
|
@@ -292,19 +346,34 @@ export class AppRailPresenter extends Component {
|
|
|
292
346
|
offset: 6
|
|
293
347
|
});
|
|
294
348
|
const actualPanel = dropdown.getPanel();
|
|
349
|
+
// Escape arrives through the overlay stack, which offers it to the topmost flyout only. The
|
|
350
|
+
// rail returns focus itself, so the trigger's focus does not reopen what Escape just closed,
|
|
351
|
+
// and a flyout is reached with the arrow keys rather than Tab: every tile is its own Tab stop.
|
|
352
|
+
dropdown._configure({
|
|
353
|
+
tabReach: false,
|
|
354
|
+
onEscape: () => {
|
|
355
|
+
const hadFocus = actualPanel.contains(document.activeElement);
|
|
356
|
+
this.closeFlyout(key);
|
|
357
|
+
if (hadFocus) this._restoreFocus(trigger);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
295
360
|
actualPanel.classList.add('zx-app-rail__popover');
|
|
296
361
|
// The panel is portaled out of the rail, so its stylesheet cannot ask which edge the rail
|
|
297
|
-
// occupies. Presentation only: it
|
|
298
|
-
// on a right-hand rail, exactly as the expanded tree does.
|
|
362
|
+
// occupies. Presentation only: it tells the panel's styles which side it opened from.
|
|
299
363
|
actualPanel.dataset.side = this.el.dataset.side;
|
|
300
364
|
actualPanel.setAttribute('role', 'region');
|
|
301
365
|
actualPanel.setAttribute('aria-label', this._message('appRail.subNavigation', '%1 sub-navigation', item.label));
|
|
302
366
|
const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
|
|
303
367
|
this._flyouts.set(key, flyout);
|
|
368
|
+
// While a tile's own flyout is open its tooltip says nothing the flyout's title does not, and
|
|
369
|
+
// would take the first Escape meant for the flyout.
|
|
370
|
+
const tooltip = parentKey === null ? this._tooltipById.get(String(item.id)) : null;
|
|
304
371
|
dropdown.on('open', () => {
|
|
372
|
+
tooltip?.disable();
|
|
305
373
|
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: true });
|
|
306
374
|
});
|
|
307
375
|
dropdown.on('close', () => {
|
|
376
|
+
tooltip?.enable();
|
|
308
377
|
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: false });
|
|
309
378
|
});
|
|
310
379
|
|
|
@@ -394,20 +463,6 @@ export class AppRailPresenter extends Component {
|
|
|
394
463
|
const controls = actions(flyout.panel);
|
|
395
464
|
const current = /** @type {Element|null} */ (event.target)?.closest?.('a, button');
|
|
396
465
|
const index = controls.indexOf(current);
|
|
397
|
-
if (event.key === 'Escape') {
|
|
398
|
-
event.preventDefault();
|
|
399
|
-
event.stopPropagation();
|
|
400
|
-
const nested = [...this._flyouts.values()].find((candidate) =>
|
|
401
|
-
candidate.trigger === current && candidate.dropdown.isOpen());
|
|
402
|
-
if (nested) {
|
|
403
|
-
this.closeFlyout(nested.key);
|
|
404
|
-
this._restoreFocus(nested.trigger);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
this.closeFlyout(flyout.key);
|
|
408
|
-
this._restoreFocus(flyout.trigger);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
466
|
const nested = [...this._flyouts.values()].find((candidate) => candidate.trigger === current);
|
|
412
467
|
const into = this.el.dataset.side === 'right' ? 'ArrowLeft' : 'ArrowRight';
|
|
413
468
|
const out = into === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight';
|
|
@@ -417,7 +472,8 @@ export class AppRailPresenter extends Component {
|
|
|
417
472
|
this.openFlyout(nested.key, { focus: true });
|
|
418
473
|
return;
|
|
419
474
|
}
|
|
420
|
-
|
|
475
|
+
// Out of a nested flyout, or out of a top-level one beside a vertical rail, back to its trigger.
|
|
476
|
+
if ((flyout.parentKey !== null || this.el.dataset.orientation === 'vertical') && event.key === out) {
|
|
421
477
|
event.preventDefault();
|
|
422
478
|
event.stopPropagation();
|
|
423
479
|
this.closeFlyout(flyout.key);
|
|
@@ -607,6 +663,22 @@ class RailListenerScope extends Component {
|
|
|
607
663
|
}
|
|
608
664
|
}
|
|
609
665
|
|
|
666
|
+
/** Marks the boundary of a section in a flattened rail. */
|
|
667
|
+
const SECTION_BREAK = Object.freeze({});
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Flattens sections for a rail: a section's children take its place. With `labels`, each section
|
|
671
|
+
* leaves its own item ahead of its children, for a flyout to caption; otherwise a section break.
|
|
672
|
+
* @param {AppNavItem[]} items Normalized items.
|
|
673
|
+
* @param {boolean} [labels=false] Keep section items as captions.
|
|
674
|
+
* @returns {AppNavItem[]}
|
|
675
|
+
*/
|
|
676
|
+
function railEntries(items, labels = false) {
|
|
677
|
+
return items.flatMap((item) => (item.section
|
|
678
|
+
? [labels ? item : /** @type {AppNavItem} */ (SECTION_BREAK), ...railEntries(item.children ?? [], labels)]
|
|
679
|
+
: [item]));
|
|
680
|
+
}
|
|
681
|
+
|
|
610
682
|
/**
|
|
611
683
|
* Normalizes an application navigation tree without mutating caller data.
|
|
612
684
|
* @param {unknown} items Candidate tree.
|
|
@@ -614,13 +686,21 @@ class RailListenerScope extends Component {
|
|
|
614
686
|
*/
|
|
615
687
|
export function normalizeAppItems(items) {
|
|
616
688
|
if (!Array.isArray(items)) return [];
|
|
617
|
-
return items.filter((item) => item && typeof item === 'object' && item.
|
|
618
|
-
|
|
689
|
+
return items.filter((item) => item && typeof item === 'object' && item.label != null
|
|
690
|
+
&& (item.id != null || item.section === true))
|
|
691
|
+
.map((item, index) => {
|
|
619
692
|
const normalized = {
|
|
620
693
|
...item,
|
|
694
|
+
// A section is addressed by nothing, so one written without an id gets a stable one.
|
|
695
|
+
id: item.id ?? `section:${index}:${item.label}`,
|
|
621
696
|
label: String(item.label),
|
|
622
697
|
children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
|
|
623
698
|
};
|
|
699
|
+
if (item.section === true) {
|
|
700
|
+
normalized.section = true;
|
|
701
|
+
delete normalized.href;
|
|
702
|
+
return normalized;
|
|
703
|
+
}
|
|
624
704
|
if (item.href == null) return normalized;
|
|
625
705
|
const href = safeHref(item.href);
|
|
626
706
|
if (href !== null) normalized.href = href;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h, safeHref } from '../core/dom.js';
|
|
2
2
|
import { icon } from '../core/icons.js';
|
|
3
|
+
import { uid } from '../core/util.js';
|
|
3
4
|
|
|
4
5
|
/** @typedef {'menuitem'|'menuitemcheckbox'|'menuitemradio'} MenuItemRole */
|
|
5
6
|
/**
|
|
@@ -31,7 +32,27 @@ import { icon } from '../core/icons.js';
|
|
|
31
32
|
* @property {string} label Visible heading.
|
|
32
33
|
*/
|
|
33
34
|
/** @typedef {{type: 'separator'}} MenuSeparator */
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* One option of a menu choice.
|
|
37
|
+
* @typedef {Object} MenuChoiceOption
|
|
38
|
+
* @property {unknown} value Value the choice takes.
|
|
39
|
+
* @property {string} label Accessible name and tooltip; the option shows its glyph or icon.
|
|
40
|
+
* @property {string} [icon] Kernel icon name.
|
|
41
|
+
* @property {Node|(() => Node)} [glyph] A pictogram drawn instead of an icon.
|
|
42
|
+
* @property {boolean} [disabled=false] Whether the option is unavailable.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* A setting chosen in place: a labelled row of two to a few options, one checked — a theme, a
|
|
46
|
+
* layout. Choosing an option keeps the menu open, so its effect shows.
|
|
47
|
+
* @typedef {Object} MenuChoice
|
|
48
|
+
* @property {'choice'} type Entry kind.
|
|
49
|
+
* @property {string} label Visible row label.
|
|
50
|
+
* @property {unknown} [value] Value of the checked option.
|
|
51
|
+
* @property {MenuChoiceOption[]} options The options, in order.
|
|
52
|
+
* @property {(value: unknown, entry: MenuChoice, controller: MenuController) => void} [onselect]
|
|
53
|
+
* Called with the chosen option's value.
|
|
54
|
+
*/
|
|
55
|
+
/** @typedef {MenuItem|MenuHeading|MenuSeparator|MenuChoice|'-'} MenuEntry */
|
|
35
56
|
|
|
36
57
|
export const MENU_ITEM_SELECTOR = [
|
|
37
58
|
'[role="menuitem"]',
|
|
@@ -46,7 +67,7 @@ export const MENU_ITEM_SELECTOR = [
|
|
|
46
67
|
*/
|
|
47
68
|
export function isMenuItem(entry) {
|
|
48
69
|
return Boolean(entry && entry !== '-' && typeof entry === 'object'
|
|
49
|
-
&& !['heading', 'separator'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
|
|
70
|
+
&& !['heading', 'separator', 'choice'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
/**
|
|
@@ -64,9 +85,11 @@ export function menuItemRole(item) {
|
|
|
64
85
|
* Builds one menu's entries with shared structure and link safety.
|
|
65
86
|
* @param {MenuEntry[]} entries Entry definitions.
|
|
66
87
|
* @param {string} block CSS block name, without the `zx-` prefix.
|
|
88
|
+
* @param {{choices?: boolean}} [options={}] Whether the menu handles `choice` entries; a menu that
|
|
89
|
+
* does not leaves them out rather than drawing a row that does nothing.
|
|
67
90
|
* @returns {HTMLElement[]}
|
|
68
91
|
*/
|
|
69
|
-
export function renderMenuEntries(entries, block) {
|
|
92
|
+
export function renderMenuEntries(entries, block, { choices = false } = {}) {
|
|
70
93
|
const nodes = [];
|
|
71
94
|
entries.forEach((entry, index) => {
|
|
72
95
|
if (entry === '-' || (entry && typeof entry === 'object' && entry.type === 'separator')) {
|
|
@@ -80,6 +103,10 @@ export function renderMenuEntries(entries, block) {
|
|
|
80
103
|
}, String(entry.label ?? '')));
|
|
81
104
|
return;
|
|
82
105
|
}
|
|
106
|
+
if (entry && typeof entry === 'object' && entry.type === 'choice') {
|
|
107
|
+
if (choices) nodes.push(renderChoice(/** @type {MenuChoice} */ (entry), index, block));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
83
110
|
if (!isMenuItem(entry)) return;
|
|
84
111
|
|
|
85
112
|
const role = menuItemRole(entry);
|
|
@@ -140,3 +167,34 @@ export function renderMenuEntries(entries, block) {
|
|
|
140
167
|
});
|
|
141
168
|
return nodes;
|
|
142
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A choice row: its label, then a segmented group of `menuitemradio` options. The group is named
|
|
173
|
+
* by the label, so a screen reader hears "Theme, Dark, radio, checked, 2 of 3".
|
|
174
|
+
* @param {MenuChoice} entry Choice definition.
|
|
175
|
+
* @param {number} index Entry index.
|
|
176
|
+
* @param {string} block CSS block name, without the `zx-` prefix.
|
|
177
|
+
* @returns {HTMLElement}
|
|
178
|
+
*/
|
|
179
|
+
function renderChoice(entry, index, block) {
|
|
180
|
+
const labelId = uid(`zx-${block}-choice`);
|
|
181
|
+
const options = Array.isArray(entry.options) ? entry.options : [];
|
|
182
|
+
return h('div', { class: `zx-${block}__choice`, role: 'group', ariaLabelledby: labelId },
|
|
183
|
+
h('span', { class: `zx-${block}__choice-label`, id: labelId }, String(entry.label ?? '')),
|
|
184
|
+
h('span', { class: `zx-${block}__choice-options` }, options.map((option, position) => {
|
|
185
|
+
const glyph = typeof option.glyph === 'function' ? option.glyph() : option.glyph;
|
|
186
|
+
return h('button', {
|
|
187
|
+
class: `zx-${block}__choice-option`,
|
|
188
|
+
type: 'button',
|
|
189
|
+
role: 'menuitemradio',
|
|
190
|
+
tabindex: '-1',
|
|
191
|
+
title: String(option.label ?? ''),
|
|
192
|
+
ariaLabel: String(option.label ?? ''),
|
|
193
|
+
ariaChecked: String(Object.is(option.value, entry.value)),
|
|
194
|
+
ariaDisabled: option.disabled ? 'true' : null,
|
|
195
|
+
'data-menu-item': String(index),
|
|
196
|
+
'data-menu-option': String(position)
|
|
197
|
+
}, glyph && typeof glyph === 'object' && 'nodeType' in glyph ? glyph
|
|
198
|
+
: option.icon ? icon(option.icon) : String(option.label ?? ''));
|
|
199
|
+
})));
|
|
200
|
+
}
|
package/src/zeyos/connect.js
CHANGED
|
@@ -107,8 +107,12 @@ function isRecord(value) {
|
|
|
107
107
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The text a failed request is reported with: the error's own message, or a generic sentence.
|
|
112
|
+
* @param {unknown} error Rejection reason.
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function errorMessage(error) {
|
|
112
116
|
if (error instanceof Error && error.message) return error.message;
|
|
113
117
|
if (error && typeof error === 'object' && typeof error.message === 'string') return error.message;
|
|
114
118
|
return String(error ?? 'ZeyOS request failed');
|
package/src/zeyos/form.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Form, Message, Select } from '../index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { overlayScope } from '../core/overlay-host.js';
|
|
3
|
+
import { connect, errorMessage } from './connect.js';
|
|
4
|
+
import {
|
|
5
|
+
calendarDayToUnixSeconds, dateToUnixSeconds, unixSecondsToCalendarDay, unixSecondsToDate
|
|
6
|
+
} from './query.js';
|
|
4
7
|
import { fieldToZxField, resolveFields } from './schema.js';
|
|
5
8
|
import { buildZeyosSelectConfig } from './select.js';
|
|
6
9
|
|
|
@@ -9,11 +12,21 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
9
12
|
* @property {string[]} [fields] Curated ordered field allow-list.
|
|
10
13
|
* @property {string[]} [exclude] Field ids to omit.
|
|
11
14
|
* @property {Record<string, string>} [labels] Per-field label overrides.
|
|
15
|
+
* @property {Record<string, string>} [formats] Per-field semantic format overrides, such as
|
|
16
|
+
* `{ duedate: 'date' }` for a Unix-second field that holds a calendar day.
|
|
17
|
+
* @property {string} [timeZone] IANA time zone of the ZeyOS instance. A date-only field's stored
|
|
18
|
+
* midnight is read as the calendar day it names in this zone, and a picked day is saved as midnight
|
|
19
|
+
* in it; defaults to the runtime's own zone.
|
|
12
20
|
* @property {string} [title=''] Fieldset legend.
|
|
13
21
|
* @property {1|2|3} [columns=2] Fieldset column count.
|
|
14
22
|
* @property {Record<string, unknown>} [value] Initial record using ZeyOS storage values.
|
|
15
23
|
* @property {unknown} [id] Initial record id to load.
|
|
16
24
|
* @property {(record: Record<string, unknown>) => unknown|Promise<unknown>} [onSaved] Success callback.
|
|
25
|
+
* @property {Element|string|null} [scope=null] Element or selector whose theme scope shows the
|
|
26
|
+
* binding's toasts — the save confirmation and the report of a failed request. Null uses the form's
|
|
27
|
+
* own element, so a form inside a scoped shell or dialog toasts inside that scope.
|
|
28
|
+
* @property {boolean} [notify=true] Whether the binding raises toasts. `false` keeps it silent:
|
|
29
|
+
* `save()`, `load()`, and `ready` still resolve or reject, and reporting is the caller's.
|
|
17
30
|
*/
|
|
18
31
|
|
|
19
32
|
/**
|
|
@@ -22,8 +35,10 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
22
35
|
* @property {Record<string, Record<string, any>>} fields Field descriptors keyed by id.
|
|
23
36
|
* @property {Array<{id: string, resource: string}>} entityFields Referenced-resource fields.
|
|
24
37
|
* @property {Record<string, any>} formOptions Plain Zx Form options.
|
|
25
|
-
* @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix
|
|
26
|
-
*
|
|
38
|
+
* @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix
|
|
39
|
+
* seconds to Date values: a date-only field to local midnight of its calendar day in `timeZone`.
|
|
40
|
+
* @property {(values: Record<string, unknown>) => Record<string, unknown>} mapToRecord Maps Date and
|
|
41
|
+
* enum values to API storage values: a date-only field to midnight of its day in `timeZone`.
|
|
27
42
|
*/
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -31,7 +46,8 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
31
46
|
* @property {Form} form Generated Zx form.
|
|
32
47
|
* @property {Promise<Record<string, unknown>|null>} ready Initial value/load readiness.
|
|
33
48
|
* @property {(id: unknown) => Promise<Record<string, unknown>>} load Loads and maps one record.
|
|
34
|
-
* @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the
|
|
49
|
+
* @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the
|
|
50
|
+
* record, then confirms with a toast in the form's scope unless `notify` is false.
|
|
35
51
|
* @property {() => Form} getForm Returns the generated Zx Form.
|
|
36
52
|
* @property {() => void} destroy Destroys the form and all owned fields/selects.
|
|
37
53
|
*/
|
|
@@ -39,7 +55,8 @@ import { buildZeyosSelectConfig } from './select.js';
|
|
|
39
55
|
/**
|
|
40
56
|
* Builds the DOM-free form configuration consumed by {@link zeyosForm}. Entity descriptors retain
|
|
41
57
|
* the `zxselect` type and receive the same async options used by {@link zeyosSelect}; enum options
|
|
42
|
-
* come from normalized schema metadata, and the returned mappers handle
|
|
58
|
+
* come from normalized schema metadata with readable labels, and the returned mappers handle
|
|
59
|
+
* Unix-second dates — date-only fields as calendar days in `opts.timeZone`.
|
|
43
60
|
* @param {Record<string, any>} client Injected ZeyOS client instance.
|
|
44
61
|
* @param {string} resource Resource name such as `transactions`.
|
|
45
62
|
* @param {ZeyosFormOptions} [opts={}] Schema curation and layout options.
|
|
@@ -49,9 +66,14 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
|
|
|
49
66
|
const fieldMeta = resolveFields(client, resource, opts);
|
|
50
67
|
const fields = {};
|
|
51
68
|
const entityFields = [];
|
|
69
|
+
/** @type {Map<string, 'date'|'datetime'>} */
|
|
70
|
+
const dateKinds = new Map();
|
|
52
71
|
|
|
53
72
|
for (const meta of fieldMeta) {
|
|
54
73
|
const descriptor = fieldToZxField(meta);
|
|
74
|
+
if (descriptor.type === 'date' || descriptor.type === 'datetime') {
|
|
75
|
+
dateKinds.set(String(meta.id), descriptor.type);
|
|
76
|
+
}
|
|
55
77
|
const referencedResource = descriptor.type === 'zxselect' && typeof descriptor.props?.entity === 'string'
|
|
56
78
|
? descriptor.props.entity
|
|
57
79
|
: null;
|
|
@@ -78,16 +100,18 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
|
|
|
78
100
|
fields
|
|
79
101
|
}]
|
|
80
102
|
},
|
|
81
|
-
mapFromRecord: (record) => mapFromRecord(record, fieldMeta),
|
|
82
|
-
mapToRecord: (values) => mapToRecord(values, fieldMeta)
|
|
103
|
+
mapFromRecord: (record) => mapFromRecord(record, fieldMeta, dateKinds),
|
|
104
|
+
mapToRecord: (values) => mapToRecord(values, fieldMeta, dateKinds)
|
|
83
105
|
};
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
87
109
|
* Generates a schema-driven Zx Form and a small async binding object. `load(id)` hydrates entity
|
|
88
110
|
* labels and converts Unix seconds to Date objects. `save()` validates, maps values back to API
|
|
89
|
-
* storage types, calls create/update through the WP14 connection facade,
|
|
90
|
-
*
|
|
111
|
+
* storage types, calls create/update through the WP14 connection facade, invokes
|
|
112
|
+
* `onSaved(record)`, and shows a success toast. Its toasts — that confirmation and the report of a
|
|
113
|
+
* failed request — appear in the theme scope of `opts.scope` or of the form itself, and
|
|
114
|
+
* `notify: false` silences them. Await `ready` when `opts.id` or `opts.value` is supplied.
|
|
91
115
|
* @param {Record<string, any>} client Injected ZeyOS client instance.
|
|
92
116
|
* @param {string} resource Resource name such as `transactions`.
|
|
93
117
|
* @param {ZeyosFormOptions} [opts={}] Schema, initial value, and save options.
|
|
@@ -97,8 +121,14 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
97
121
|
if (opts.onSaved != null && typeof opts.onSaved !== 'function') {
|
|
98
122
|
throw new TypeError('zeyosForm onSaved must be a function');
|
|
99
123
|
}
|
|
124
|
+
const notify = opts.notify !== false;
|
|
100
125
|
const config = buildZeyosFormConfig(client, resource, opts);
|
|
101
|
-
|
|
126
|
+
// The caller's scope, or the form's own element, decides where a toast appears — never whatever
|
|
127
|
+
// element happens to hold focus when a request settles.
|
|
128
|
+
const toastScope = () => opts.scope ?? form.el;
|
|
129
|
+
const facade = connect(client, {
|
|
130
|
+
onError: (error) => notify ? Message.error(errorMessage(error), { scope: toastScope() }) : undefined
|
|
131
|
+
});
|
|
102
132
|
const form = new Form(null, config.formOptions);
|
|
103
133
|
let currentId = opts.value?.ID ?? opts.id ?? null;
|
|
104
134
|
let destroyed = false;
|
|
@@ -133,6 +163,10 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
133
163
|
if (destroyed) return Promise.reject(new Error('Cannot save a destroyed ZeyOS form'));
|
|
134
164
|
if (savePending) return savePending;
|
|
135
165
|
if (!form.submit()) return Promise.resolve(null);
|
|
166
|
+
// Resolved now, while the form is in place, and to the theme scope rather than the open
|
|
167
|
+
// dialog the overlay host would be: `onSaved` may close and destroy that dialog, and a toast
|
|
168
|
+
// parked inside it would fall back to an unstyled <body>.
|
|
169
|
+
const host = notify ? overlayScope(toastScope()) : null;
|
|
136
170
|
|
|
137
171
|
savePending = (async () => {
|
|
138
172
|
let payload;
|
|
@@ -157,7 +191,7 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
157
191
|
throw error;
|
|
158
192
|
}
|
|
159
193
|
}
|
|
160
|
-
Message.success(`${recordLabel(record, resource)} saved successfully
|
|
194
|
+
if (host) Message.success(`${recordLabel(record, resource)} saved successfully.`, { scope: host });
|
|
161
195
|
return record;
|
|
162
196
|
})().finally(() => {
|
|
163
197
|
savePending = null;
|
|
@@ -183,27 +217,41 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
183
217
|
};
|
|
184
218
|
}
|
|
185
219
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
220
|
+
/**
|
|
221
|
+
* @param {Record<string, unknown>} record
|
|
222
|
+
* @param {Record<string, any>[]} meta
|
|
223
|
+
* @param {Map<string, 'date'|'datetime'>} dateKinds
|
|
224
|
+
* @returns {Record<string, unknown>}
|
|
225
|
+
*/
|
|
226
|
+
function mapFromRecord(record, meta, dateKinds) {
|
|
188
227
|
const values = {};
|
|
189
228
|
for (const field of meta) {
|
|
190
229
|
const id = String(field.id);
|
|
191
230
|
const value = record[id];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
231
|
+
const kind = dateKinds.get(id);
|
|
232
|
+
if (!kind || value == null || value === '') values[id] = value;
|
|
233
|
+
else if (kind === 'date') values[id] = unixSecondsToCalendarDay(/** @type {number|string} */ (value), field.timeZone);
|
|
234
|
+
else values[id] = unixSecondsToDate(/** @type {number|string} */ (value));
|
|
195
235
|
}
|
|
196
236
|
return values;
|
|
197
237
|
}
|
|
198
238
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
239
|
+
/**
|
|
240
|
+
* @param {Record<string, unknown>} values
|
|
241
|
+
* @param {Record<string, any>[]} meta
|
|
242
|
+
* @param {Map<string, 'date'|'datetime'>} dateKinds
|
|
243
|
+
* @returns {Record<string, unknown>}
|
|
244
|
+
*/
|
|
245
|
+
function mapToRecord(values, meta, dateKinds) {
|
|
201
246
|
const record = {};
|
|
202
247
|
for (const field of meta) {
|
|
203
248
|
const id = String(field.id);
|
|
204
249
|
let value = values[id];
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
const kind = dateKinds.get(id);
|
|
251
|
+
if (kind) {
|
|
252
|
+
if (value == null || value === '') value = null;
|
|
253
|
+
else if (kind === 'date') value = calendarDayToUnixSeconds(/** @type {Date} */ (value), field.timeZone);
|
|
254
|
+
else value = dateToUnixSeconds(/** @type {Date} */ (value));
|
|
207
255
|
} else if (hasEnum(field) && isNumericType(field.type) && value !== '' && value != null) {
|
|
208
256
|
const numeric = Number(value);
|
|
209
257
|
if (Number.isFinite(numeric)) value = numeric;
|
|
@@ -213,12 +261,6 @@ function mapToRecord(values, meta) {
|
|
|
213
261
|
return record;
|
|
214
262
|
}
|
|
215
263
|
|
|
216
|
-
/** @param {Record<string, any>} meta @returns {boolean} */
|
|
217
|
-
function isDateField(meta) {
|
|
218
|
-
const descriptor = fieldToZxField(meta);
|
|
219
|
-
return descriptor.type === 'date' || descriptor.type === 'datetime';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
264
|
/** @param {Record<string, any>} meta @returns {boolean} */
|
|
223
265
|
function hasEnum(meta) {
|
|
224
266
|
return meta.enums != null || meta.enum != null;
|
package/src/zeyos/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
} from './schema.js';
|
|
9
9
|
export {
|
|
10
10
|
buildListQuery, tableSortToQuery, dataFilterStateToFilters,
|
|
11
|
-
dateToUnixSeconds, unixSecondsToDate
|
|
11
|
+
dateToUnixSeconds, unixSecondsToDate, calendarDayToUnixSeconds, unixSecondsToCalendarDay
|
|
12
12
|
} from './query.js';
|
|
13
13
|
export { connect } from './connect.js';
|
|
14
14
|
export {
|
|
@@ -20,6 +20,9 @@ export {
|
|
|
20
20
|
ZEYOS_LAUNCHER_APPLICATIONS, buildZeyosLauncherConfig, zeyosLauncher,
|
|
21
21
|
normalizeZeyosLauncherApplications, normalizeZeyosLauncherRecords, zeyosActiveIdentifier
|
|
22
22
|
} from './launcher.js';
|
|
23
|
+
export {
|
|
24
|
+
zeyosModuleItems, buildZeyosModuleBarConfig, buildZeyosAppSidebarConfig, zeyosModuleBar
|
|
25
|
+
} from './module-bar.js';
|
|
23
26
|
export {
|
|
24
27
|
zeyosSelect, buildZeyosSelectConfig, zeyosEntitySelect, buildZeyosEntitySelectConfig
|
|
25
28
|
} from './select.js';
|
package/src/zeyos/launcher.js
CHANGED
|
@@ -224,8 +224,13 @@ export function zeyosActiveIdentifier(state = {}) {
|
|
|
224
224
|
return String(state.module ?? '');
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/**
|
|
228
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Normalizes one application of the ZeyOS shell catalogue: a `[module, label]` pair, a
|
|
229
|
+
* `[ID, name, module, color]` fork, a weblet row, or an object. Shared with the module bar.
|
|
230
|
+
* @param {unknown} raw @param {string|null} fallbackId @param {unknown} fallbackLabel
|
|
231
|
+
* @returns {Record<string, any>|null}
|
|
232
|
+
*/
|
|
233
|
+
export function normalizeApplication(raw, fallbackId, fallbackLabel) {
|
|
229
234
|
if (Array.isArray(raw)) {
|
|
230
235
|
if (raw.length < 2) return null;
|
|
231
236
|
if (raw.length === 2) {
|
|
@@ -305,7 +310,7 @@ function normalizeRecord(raw, group, query) {
|
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
/** @param {unknown} raw @param {unknown} resolver @returns {Record<string, any>} */
|
|
308
|
-
function mergeDestination(raw, resolver) {
|
|
313
|
+
export function mergeDestination(raw, resolver) {
|
|
309
314
|
const direct = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
310
315
|
const resolved = typeof resolver === 'function' ? resolver(raw) : null;
|
|
311
316
|
const destination = resolved && typeof resolved === 'object' ? resolved : {};
|