@zeyos/zx 5.0.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 +164 -0
- package/README.md +2 -1
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +9 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
- 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/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/dropdown/dropdown.d.ts +22 -7
- package/dist/types/components/field/field.d.ts +30 -0
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +7 -0
- 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/launcher/launcher.d.ts +8 -0
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
- package/dist/types/components/menu-button/menu-button.d.ts +6 -0
- package/dist/types/components/message/message.d.ts +14 -1
- package/dist/types/components/modal/modal.d.ts +30 -0
- 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/select.d.ts +6 -0
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/table/table.d.ts +265 -17
- 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/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/overlay-host.d.ts +28 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/index.d.ts +1 -0
- 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 +31 -5
- package/dist/types/zeyos/table.d.ts +245 -8
- package/dist/types/zeyos/view.d.ts +16 -2
- package/dist/zx-zeyos.esm.js +4422 -430
- package/dist/zx-zeyos.esm.min.js +5 -5
- package/dist/zx.css +1417 -134
- package/dist/zx.esm.js +6986 -830
- package/dist/zx.esm.min.js +8 -8
- package/dist/zx.global.js +6986 -830
- package/dist/zx.global.min.js +8 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +794 -17
- package/docs/llms.md +503 -63
- package/docs/llms.txt +6 -2
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-list/activity-list.js +27 -1
- package/src/components/app-icon/app-icon.css +13 -2
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +19 -0
- package/src/components/app-sidebar/app-sidebar.css +204 -3
- package/src/components/app-sidebar/app-sidebar.js +646 -40
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/calendar/calendar.css +1 -1
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +5 -5
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/code-editor/code-editor.css +1 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-input.css +1 -1
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/time-picker.css +2 -2
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.js +157 -72
- package/src/components/field/field.js +33 -3
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.js +10 -59
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +300 -8
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +1 -1
- package/src/components/kanban-view/kanban-view.css +3 -3
- package/src/components/kanban-view/kanban-view.js +5 -2
- package/src/components/launcher/launcher.css +1 -1
- package/src/components/launcher/launcher.js +29 -3
- package/src/components/list-toolbar/list-toolbar.js +39 -13
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +63 -0
- package/src/components/menu-button/menu-button.js +97 -11
- package/src/components/message/message.js +60 -20
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +66 -13
- 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/number-field/field-adapter.js +2 -1
- package/src/components/pagination/pagination.css +5 -5
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +3 -2
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +1 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/select.js +9 -0
- package/src/components/sheet/sheet.css +1 -1
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +2 -2
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +1 -1
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +9 -5
- package/src/components/table/table.css +86 -11
- package/src/components/table/table.js +628 -57
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tooltip/tooltip.js +35 -17
- package/src/components/tree/tree.css +1 -1
- 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/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +12 -2
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/overlay-host.js +64 -9
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +5 -2
- package/src/internal/app-rail.js +134 -53
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/form.js +5 -3
- package/src/zeyos/index.js +3 -0
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +44 -13
- package/src/zeyos/table.js +220 -80
- package/src/zeyos/view.js +43 -93
- package/styles/base.css +16 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +56 -28
- package/styles/zx.css +3 -0
package/src/zeyos/query.js
CHANGED
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
* @property {string} [rawField] Raw ZeyOS field when the UI id is an alias.
|
|
21
21
|
* @property {string[]} [fields] Raw fields combined as an OR group.
|
|
22
22
|
* @property {string} [operator] ZeyOS comparison operator such as `>=` or `IN`.
|
|
23
|
-
* @property {string} [format] Semantic value format; date/datetime values become Unix seconds.
|
|
24
|
-
*
|
|
23
|
+
* @property {string} [format] Semantic value format; date/datetime values become Unix seconds. With
|
|
24
|
+
* a `timeZone` conversion option, a `date` (date-only) value is a calendar day and becomes that
|
|
25
|
+
* day's midnight in the zone, like a ZeyOS form stores it; `datetime` values stay instants.
|
|
26
|
+
* @property {string} [type] Semantic value type, read like `format`.
|
|
25
27
|
* @property {boolean} [includeEmpty=false] Preserve an empty-string value instead of treating it as inactive UI state.
|
|
26
28
|
* @property {boolean} [includeNull=false] Preserve null instead of treating it as inactive UI state.
|
|
27
29
|
* @property {(value: unknown, state: Record<string, unknown>) => unknown} [transform] Value transform.
|
|
@@ -225,6 +227,16 @@ export function tableSortToQuery(sort) {
|
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/**
|
|
231
|
+
* @typedef {Object} FilterConversionOptions
|
|
232
|
+
* @property {string|null} [timeZone] IANA zone of the ZeyOS instance. When given, values of
|
|
233
|
+
* `date` (date-only) definitions are calendar days: a `Date` counts by its local year, month, and
|
|
234
|
+
* day, a `YYYY-MM-DD` string by its digits, and either becomes midnight of that day in the zone —
|
|
235
|
+
* exactly what `calendarDayToUnixSeconds()` and `zeyosForm()` store, so a viewer in another zone
|
|
236
|
+
* filters `duedate` by the day they picked. `null` names the runtime's own zone. Omitted, dates
|
|
237
|
+
* convert as exact instants, as they always have.
|
|
238
|
+
*/
|
|
239
|
+
|
|
228
240
|
/**
|
|
229
241
|
* Converts DataFilter state to ZeyOS field predicates. Definitions may rename UI ids,
|
|
230
242
|
* add comparison operators, combine several fields with OR, or transform values. Blank/null
|
|
@@ -232,12 +244,17 @@ export function tableSortToQuery(sort) {
|
|
|
232
244
|
* the distinct ZeyOS meanings. Zero and false are always retained.
|
|
233
245
|
* @param {Record<string, unknown>} state DataFilter state keyed by definition id.
|
|
234
246
|
* @param {ServerFilterDefinition[]|Record<string, string|ServerFilterDefinition>} [defs=[]] Server mappings.
|
|
247
|
+
* @param {FilterConversionOptions} [options={}] Conversion options, such as the instance zone.
|
|
235
248
|
* @returns {Record<string, unknown>} ZeyOS `filters` object (plural at the call site).
|
|
249
|
+
* @throws {RangeError} When `options.timeZone` names a zone the runtime does not know.
|
|
236
250
|
*/
|
|
237
|
-
export function dataFilterStateToFilters(state, defs = []) {
|
|
251
|
+
export function dataFilterStateToFilters(state, defs = [], options = {}) {
|
|
238
252
|
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
239
253
|
throw new TypeError('DataFilter state must be an object');
|
|
240
254
|
}
|
|
255
|
+
const calendarDays = Boolean(options) && typeof options === 'object'
|
|
256
|
+
&& Object.prototype.hasOwnProperty.call(options, 'timeZone');
|
|
257
|
+
const zone = calendarDays ? normalizeTimeZone(options.timeZone) : undefined;
|
|
241
258
|
const definitions = normalizeDefinitions(state, defs);
|
|
242
259
|
const filters = {};
|
|
243
260
|
let groupIndex = 0;
|
|
@@ -259,7 +276,7 @@ export function dataFilterStateToFilters(state, defs = []) {
|
|
|
259
276
|
|
|
260
277
|
let value = typeof definition.transform === 'function'
|
|
261
278
|
? definition.transform(original, state)
|
|
262
|
-
: normalizeFilterValue(original, definition);
|
|
279
|
+
: normalizeFilterValue(original, definition, calendarDays ? { timeZone: zone } : null);
|
|
263
280
|
if (value === undefined) continue;
|
|
264
281
|
if (definition.operator) value = { [definition.operator]: value };
|
|
265
282
|
|
|
@@ -327,16 +344,30 @@ function normalizeDefinitions(state, defs) {
|
|
|
327
344
|
return Object.keys(state).map((id) => ({ id, field: id }));
|
|
328
345
|
}
|
|
329
346
|
|
|
330
|
-
/**
|
|
331
|
-
|
|
347
|
+
/**
|
|
348
|
+
* @param {unknown} value Filter value.
|
|
349
|
+
* @param {ServerFilterDefinition} definition Server mapping.
|
|
350
|
+
* @param {{timeZone: string|undefined}|null} calendar Zone for date-only values, or null to convert
|
|
351
|
+
* every date as an instant.
|
|
352
|
+
* @returns {unknown}
|
|
353
|
+
*/
|
|
354
|
+
function normalizeFilterValue(value, definition, calendar = null) {
|
|
332
355
|
const semantic = String(definition.format ?? definition.type ?? '').toLowerCase();
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
356
|
+
if (!['date', 'datetime', 'date-time'].includes(semantic)) return value;
|
|
357
|
+
/** @param {unknown} entry @returns {unknown} */
|
|
358
|
+
const convert = (entry) => {
|
|
359
|
+
if (calendar && semantic === 'date') {
|
|
360
|
+
if (entry instanceof Date) return calendarDayToUnixSeconds(entry, calendar.timeZone);
|
|
361
|
+
const day = typeof entry === 'string' ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(entry.trim()) : null;
|
|
362
|
+
if (day) {
|
|
363
|
+
const midnight = calendarZone(calendar.timeZone).midnightOf(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
|
|
364
|
+
return Math.floor(midnight / 1000);
|
|
365
|
+
}
|
|
366
|
+
return entry;
|
|
367
|
+
}
|
|
368
|
+
return entry instanceof Date ? dateToUnixSeconds(entry) : entry;
|
|
369
|
+
};
|
|
370
|
+
return Array.isArray(value) ? value.map(convert) : convert(value);
|
|
340
371
|
}
|
|
341
372
|
|
|
342
373
|
/** @param {ZxSort|ZxSort[]|undefined} sort @param {string[]|Record<string, string>|undefined} fields @returns {ZxSort|ZxSort[]|undefined} */
|
package/src/zeyos/table.js
CHANGED
|
@@ -15,7 +15,8 @@ import { normalizeZeyosListResult } from './select.js';
|
|
|
15
15
|
* @property {string} [timeZone] IANA time zone of the ZeyOS instance. Date-only columns show the
|
|
16
16
|
* calendar day their stored midnight names in this zone; defaults to the runtime's own zone.
|
|
17
17
|
* @property {string|((row: Record<string, unknown>) => unknown)} [rowId='ID'] Row id accessor.
|
|
18
|
-
* @property {number} [pageSize=50] Server page size.
|
|
18
|
+
* @property {number} [pageSize=50] Server page size, a positive integer. `load({pageSize})` changes
|
|
19
|
+
* it later — what a `Pagination` page-size select does.
|
|
19
20
|
* @property {{id: string, dir: 'asc'|'desc'}|null} [sort=null] Initial server sort.
|
|
20
21
|
* @property {Record<string, unknown>} [filters] Initial ZeyOS predicates.
|
|
21
22
|
* @property {string} [search=''] Initial full-text query.
|
|
@@ -70,7 +71,12 @@ const TABLE_OPTION_KEYS = [
|
|
|
70
71
|
* @typedef {Object} ZeyosTableLoadOptions
|
|
71
72
|
* @property {string} [search] Replacement full-text query.
|
|
72
73
|
* @property {Record<string, unknown>} [filters] Replacement ZeyOS predicates.
|
|
73
|
-
* @property {number} [page=0] Zero-based page
|
|
74
|
+
* @property {number} [page=0] Zero-based page, a non-negative integer. Pages above zero append
|
|
75
|
+
* rows unless `replace` is set.
|
|
76
|
+
* @property {boolean} [replace] Replace the rows with this page instead of appending it — what a
|
|
77
|
+
* numbered pager needs. Page zero always replaces.
|
|
78
|
+
* @property {number} [pageSize] New page size for this and every later load; the request offset is
|
|
79
|
+
* `page * pageSize`.
|
|
74
80
|
*/
|
|
75
81
|
|
|
76
82
|
/**
|
|
@@ -91,8 +97,57 @@ const TABLE_OPTION_KEYS = [
|
|
|
91
97
|
* @property {() => Promise<ZeyosTableLoadResult>} loadMore Appends the next page when available.
|
|
92
98
|
* @property {number|null} count Current server count.
|
|
93
99
|
* @property {number} page Current zero-based page.
|
|
100
|
+
* @property {number} pageSize Current server page size.
|
|
94
101
|
* @property {boolean} hasMore Whether another page is available.
|
|
95
|
-
* @property {() => void} destroy Destroys the generated table.
|
|
102
|
+
* @property {() => void} destroy Destroys the generated table. A request still in flight is
|
|
103
|
+
* ignored when it resolves, and later loads reject.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The surface a server pager renders into: a Zx `Table`, or any RecordView.
|
|
108
|
+
* @typedef {Object} ServerPagerTarget
|
|
109
|
+
* @property {(rows: Record<string, unknown>[]) => unknown} setData Replaces the rows.
|
|
110
|
+
* @property {(rows: Record<string, unknown>[]) => unknown} addData Appends rows.
|
|
111
|
+
* @property {(loading: boolean) => unknown} setLoading Shows or hides the loading state.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {Object} ServerPagerOptions
|
|
116
|
+
* @property {Record<string, any>} client Injected ZeyOS client (for result normalization).
|
|
117
|
+
* @property {{list: (resource: string, query: Record<string, unknown>) => Promise<unknown>}} facade
|
|
118
|
+
* Connection facade from `connect(client)`.
|
|
119
|
+
* @property {string} resource Resource name.
|
|
120
|
+
* @property {Record<string, string>} projection API projection.
|
|
121
|
+
* @property {ServerPagerTarget} target Component the rows go to.
|
|
122
|
+
* @property {unknown} [pageSize=50] Initial page size; must be a positive integer.
|
|
123
|
+
* @property {{id: string, dir: 'asc'|'desc'}|null} [sort=null] Initial sort.
|
|
124
|
+
* @property {unknown} [filters={}] Initial predicates; must be a plain object.
|
|
125
|
+
* @property {unknown} [search=''] Initial full-text query.
|
|
126
|
+
* @property {string[]} [searchFields] UI search-field metadata passed to `buildListQuery`.
|
|
127
|
+
* @property {string} [label='ZeyOS binding'] Name used in error messages.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @typedef {Object} ServerPager
|
|
132
|
+
* @property {(options?: ZeyosTableLoadOptions) => Promise<ZeyosTableLoadResult>} load Loads a page.
|
|
133
|
+
* @property {(options?: ZeyosTableLoadOptions) => {page: number, pageSize: number, filters: Record<string, unknown>, search: string}} check
|
|
134
|
+
* Validates load options without applying them; throws what `load()` would reject with.
|
|
135
|
+
* @property {() => Promise<ZeyosTableLoadResult>} loadMore Appends the next page when available,
|
|
136
|
+
* else resolves without a request.
|
|
137
|
+
* @property {(sort: {id: string, dir: 'asc'|'desc'}|null) => void} setSort Replaces the sort used
|
|
138
|
+
* by the next load.
|
|
139
|
+
* @property {(state: {filters?: unknown, search?: unknown}) => void} setQuery Replaces filters and
|
|
140
|
+
* search without loading.
|
|
141
|
+
* @property {() => void} reset Forgets the loaded page, count, and `hasMore`.
|
|
142
|
+
* @property {() => void} destroy Stops rendering: in-flight responses are ignored, later loads reject.
|
|
143
|
+
* @property {boolean} destroyed Whether `destroy()` ran.
|
|
144
|
+
* @property {number|null} count Current server count.
|
|
145
|
+
* @property {number} page Current zero-based page.
|
|
146
|
+
* @property {number} pageSize Current page size.
|
|
147
|
+
* @property {boolean} hasMore Whether another page is available.
|
|
148
|
+
* @property {{id: string, dir: 'asc'|'desc'}|null} sort Current sort.
|
|
149
|
+
* @property {Record<string, unknown>} filters Copy of the current predicates.
|
|
150
|
+
* @property {string} search Current full-text query.
|
|
96
151
|
*/
|
|
97
152
|
|
|
98
153
|
/**
|
|
@@ -160,9 +215,15 @@ export function buildZeyosTableConfig(client, resource, opts = {}) {
|
|
|
160
215
|
}
|
|
161
216
|
|
|
162
217
|
/**
|
|
163
|
-
* Generates a server-backed Zx Table. Sorting automatically reloads page zero.
|
|
164
|
-
* replaces data, higher pages append it
|
|
165
|
-
*
|
|
218
|
+
* Generates a server-backed Zx Table. Sorting automatically reloads page zero. Paging is
|
|
219
|
+
* zero-based: `load({page: 0})` replaces data, higher pages append it — load-more paging, which
|
|
220
|
+
* `loadMore()` drives from `limit`/`offset` plus the returned count — and `replace: true` replaces
|
|
221
|
+
* at any page, which is what a numbered `Pagination` needs. `Pagination` is one-based, so its
|
|
222
|
+
* `change` maps as `load({page: detail.page - 1, pageSize: detail.pageSize, replace: true})`, the
|
|
223
|
+
* request offset being `(page - 1) * pageSize`; feed `result.count` back with `setTotal()`.
|
|
224
|
+
* DataFilter callers can map state with `dataFilterStateToFilters` before `setFilters()`.
|
|
225
|
+
* Paging is shared with `zeyosView()` through {@link createServerPager}: the same validation, and
|
|
226
|
+
* a response that resolves after `destroy()` never touches the destroyed table.
|
|
166
227
|
* Table presentation options (`variant`, `zebra`, `stickyHeader`, `responsive`, `emptyText`, …) and
|
|
167
228
|
* listeners are passed through to the generated Table.
|
|
168
229
|
* @param {Record<string, any>} client Injected ZeyOS client instance.
|
|
@@ -174,82 +235,156 @@ export function zeyosTable(client, resource, opts = {}) {
|
|
|
174
235
|
const config = buildZeyosTableConfig(client, resource, opts);
|
|
175
236
|
const facade = connect(client);
|
|
176
237
|
const table = new Table(null, config.tableOptions);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
238
|
+
let pager;
|
|
239
|
+
try {
|
|
240
|
+
pager = createServerPager({
|
|
241
|
+
client, facade, resource, projection: config.projection, target: table,
|
|
242
|
+
pageSize: opts.pageSize, sort: opts.sort ?? null, filters: opts.filters ?? {}, search: opts.search,
|
|
243
|
+
label: 'ZeyOS table'
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
table.destroy();
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const binding = {
|
|
251
|
+
table,
|
|
252
|
+
load: pager.load,
|
|
253
|
+
setSearch: (search) => pager.load({ search, page: 0 }),
|
|
254
|
+
setFilters: (filters) => pager.load({ filters, page: 0 }),
|
|
255
|
+
loadMore: pager.loadMore,
|
|
256
|
+
get count() { return pager.count; },
|
|
257
|
+
get page() { return pager.page; },
|
|
258
|
+
get pageSize() { return pager.pageSize; },
|
|
259
|
+
get hasMore() { return pager.hasMore; },
|
|
260
|
+
destroy: () => {
|
|
261
|
+
if (pager.destroyed) return;
|
|
262
|
+
pager.destroy();
|
|
263
|
+
table.destroy();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
table.on('sort', (event) => {
|
|
268
|
+
if (pager.destroyed) return;
|
|
269
|
+
const id = event.detail?.id;
|
|
270
|
+
pager.setSort(id == null ? null : { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' });
|
|
271
|
+
void pager.load({ page: 0 }).catch(() => { /* The connection facade already reported the failure. */ });
|
|
272
|
+
});
|
|
273
|
+
return binding;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The one server-paging loader behind {@link zeyosTable} and `zeyosView`, so the two bindings
|
|
278
|
+
* cannot drift apart. It builds the list request (`limit = pageSize`, `offset = page * pageSize`,
|
|
279
|
+
* always with `count`), renders the response into its target — page zero or `replace` replaces
|
|
280
|
+
* the rows, a higher page appends them — and keeps `count`, `page`, and `hasMore`.
|
|
281
|
+
*
|
|
282
|
+
* Only the newest request may render: an older one that resolves late is returned to its caller
|
|
283
|
+
* but never touches the target. After `destroy()` nothing renders at all — a response that arrives
|
|
284
|
+
* afterwards is ignored, the loading state is left alone — and further loads reject.
|
|
285
|
+
*
|
|
286
|
+
* Validation is strict and the same for both bindings: a page must be a non-negative integer and a
|
|
287
|
+
* page size a positive integer (`RangeError`), filters a plain object (`TypeError`). Every argument
|
|
288
|
+
* of a call is checked before any of it is applied, so a rejected call changes nothing.
|
|
289
|
+
* @param {ServerPagerOptions} options Pager configuration.
|
|
290
|
+
* @returns {ServerPager}
|
|
291
|
+
*/
|
|
292
|
+
export function createServerPager(options) {
|
|
293
|
+
const { client, facade, resource, projection, target, searchFields } = options;
|
|
294
|
+
const label = String(options.label ?? 'ZeyOS binding');
|
|
295
|
+
let pageSize = normalizePageSize(options.pageSize, label);
|
|
296
|
+
let sort = options.sort ?? null;
|
|
297
|
+
let filters = copyFilters(options.filters ?? {}, label);
|
|
298
|
+
let search = String(options.search ?? '');
|
|
299
|
+
let page = 0;
|
|
300
|
+
let count = null;
|
|
183
301
|
let more = false;
|
|
184
|
-
let
|
|
302
|
+
let sequence = 0;
|
|
303
|
+
let destroyed = false;
|
|
185
304
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
305
|
+
const query = (forPage) => buildListQuery({
|
|
306
|
+
fields: projection, sort, filters, search, searchFields,
|
|
307
|
+
limit: pageSize, offset: forPage * pageSize, count: true
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Validates a load request without applying any of it.
|
|
312
|
+
* @param {ZeyosTableLoadOptions} [request={}] Load options.
|
|
313
|
+
* @returns {{page: number, pageSize: number, filters: Record<string, unknown>, search: string}}
|
|
314
|
+
*/
|
|
315
|
+
const check = (request = {}) => {
|
|
316
|
+
const source = request && typeof request === 'object' ? request : {};
|
|
317
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(source, key);
|
|
318
|
+
return {
|
|
319
|
+
page: normalizePage(source.page ?? 0, label),
|
|
320
|
+
pageSize: has('pageSize') && source.pageSize !== undefined
|
|
321
|
+
? normalizePageSize(source.pageSize, label) : pageSize,
|
|
322
|
+
filters: has('filters') ? copyFilters(source.filters ?? {}, label) : filters,
|
|
323
|
+
search: has('search') ? String(source.search ?? '') : search
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** @param {ZeyosTableLoadOptions} [request={}] @returns {Promise<ZeyosTableLoadResult>} */
|
|
328
|
+
const load = async (request = {}) => {
|
|
329
|
+
if (destroyed) throw new Error(`${label} binding has been destroyed`);
|
|
330
|
+
// Every argument is validated before any state changes, so a call that rejects leaves the
|
|
331
|
+
// binding exactly as it was: no page size or search half-applied ahead of a bad filter.
|
|
332
|
+
const next = check(request);
|
|
333
|
+
const nextPage = next.page;
|
|
334
|
+
pageSize = next.pageSize;
|
|
335
|
+
filters = next.filters;
|
|
336
|
+
search = next.search;
|
|
337
|
+
const replace = nextPage === 0 || request.replace === true;
|
|
338
|
+
const sent = query(nextPage);
|
|
339
|
+
const mine = ++sequence;
|
|
340
|
+
target.setLoading(true);
|
|
201
341
|
try {
|
|
202
|
-
const normalized = normalizeZeyosListResult(client, await facade.list(resource,
|
|
203
|
-
const
|
|
204
|
-
const hasMore =
|
|
342
|
+
const normalized = normalizeZeyosListResult(client, await facade.list(resource, sent));
|
|
343
|
+
const total = normalized.count ?? null;
|
|
344
|
+
const hasMore = total == null
|
|
205
345
|
? normalized.data.length === pageSize
|
|
206
|
-
:
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
346
|
+
: nextPage * pageSize + normalized.data.length < total;
|
|
347
|
+
if (!destroyed && mine === sequence) {
|
|
348
|
+
page = nextPage;
|
|
349
|
+
count = total;
|
|
210
350
|
more = hasMore;
|
|
211
|
-
if (
|
|
212
|
-
else
|
|
351
|
+
if (replace) target.setData(normalized.data);
|
|
352
|
+
else target.addData(normalized.data);
|
|
213
353
|
}
|
|
214
|
-
return { data: normalized.data, count, page, hasMore, query };
|
|
354
|
+
return { data: normalized.data, count: total, page: nextPage, hasMore, query: sent };
|
|
215
355
|
} finally {
|
|
216
|
-
if (
|
|
356
|
+
if (!destroyed && mine === sequence) target.setLoading(false);
|
|
217
357
|
}
|
|
218
358
|
};
|
|
219
359
|
|
|
220
|
-
|
|
221
|
-
table,
|
|
360
|
+
return {
|
|
222
361
|
load,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
data: [],
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
get
|
|
241
|
-
get
|
|
362
|
+
check,
|
|
363
|
+
loadMore: () => more ? load({ page: page + 1 }) : (destroyed
|
|
364
|
+
? Promise.reject(new Error(`${label} binding has been destroyed`))
|
|
365
|
+
: Promise.resolve({ data: [], count, page, hasMore: false, query: query(page) })),
|
|
366
|
+
setSort: (next) => { sort = next ?? null; },
|
|
367
|
+
setQuery: (state = {}) => {
|
|
368
|
+
const nextFilters = Object.prototype.hasOwnProperty.call(state, 'filters')
|
|
369
|
+
? copyFilters(state.filters ?? {}, label) : filters;
|
|
370
|
+
if (Object.prototype.hasOwnProperty.call(state, 'search')) search = String(state.search ?? '');
|
|
371
|
+
filters = nextFilters;
|
|
372
|
+
},
|
|
373
|
+
reset: () => { page = 0; count = null; more = false; },
|
|
374
|
+
destroy: () => {
|
|
375
|
+
if (destroyed) return;
|
|
376
|
+
destroyed = true;
|
|
377
|
+
sequence += 1;
|
|
378
|
+
},
|
|
379
|
+
get destroyed() { return destroyed; },
|
|
380
|
+
get count() { return count; },
|
|
381
|
+
get page() { return page; },
|
|
382
|
+
get pageSize() { return pageSize; },
|
|
242
383
|
get hasMore() { return more; },
|
|
243
|
-
|
|
384
|
+
get sort() { return sort; },
|
|
385
|
+
get filters() { return { ...filters }; },
|
|
386
|
+
get search() { return search; }
|
|
244
387
|
};
|
|
245
|
-
|
|
246
|
-
table.on('sort', (event) => {
|
|
247
|
-
const id = event.detail?.id;
|
|
248
|
-
currentSort = id == null ? null
|
|
249
|
-
: { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
|
|
250
|
-
void load({ page: 0 }).catch(() => { /* The connection facade already reported the failure. */ });
|
|
251
|
-
});
|
|
252
|
-
return binding;
|
|
253
388
|
}
|
|
254
389
|
|
|
255
390
|
/**
|
|
@@ -315,23 +450,28 @@ function schemaHasField(client, resource, id) {
|
|
|
315
450
|
return Boolean(fields && typeof fields === 'object' && Object.prototype.hasOwnProperty.call(fields, id));
|
|
316
451
|
}
|
|
317
452
|
|
|
318
|
-
/**
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
453
|
+
/** Default server page size shared by `zeyosTable()` and `zeyosView()`. */
|
|
454
|
+
export const DEFAULT_SERVER_PAGE_SIZE = 50;
|
|
455
|
+
|
|
456
|
+
/** @param {unknown} value @param {string} [label='ZeyOS table'] @returns {number} */
|
|
457
|
+
function normalizePageSize(value, label = 'ZeyOS table') {
|
|
458
|
+
if (value === undefined || value === null) return DEFAULT_SERVER_PAGE_SIZE;
|
|
459
|
+
const number = Number(value);
|
|
460
|
+
if (!Number.isInteger(number) || number < 1) throw new RangeError(`${label} page size must be a positive integer`);
|
|
461
|
+
return number;
|
|
322
462
|
}
|
|
323
463
|
|
|
324
|
-
/** @param {unknown} value @returns {number} */
|
|
325
|
-
function normalizePage(value) {
|
|
464
|
+
/** @param {unknown} value @param {string} [label='ZeyOS table'] @returns {number} */
|
|
465
|
+
function normalizePage(value, label = 'ZeyOS table') {
|
|
326
466
|
const number = Number(value);
|
|
327
|
-
if (!Number.isInteger(number) || number < 0) throw new RangeError(
|
|
467
|
+
if (!Number.isInteger(number) || number < 0) throw new RangeError(`${label} page must be a non-negative integer`);
|
|
328
468
|
return number;
|
|
329
469
|
}
|
|
330
470
|
|
|
331
|
-
/** @param {
|
|
332
|
-
function copyFilters(filters) {
|
|
471
|
+
/** @param {unknown} filters @param {string} [label='ZeyOS table'] @returns {Record<string, unknown>} */
|
|
472
|
+
function copyFilters(filters, label = 'ZeyOS table') {
|
|
333
473
|
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
|
|
334
|
-
throw new TypeError(
|
|
474
|
+
throw new TypeError(`${label} filters must be an object`);
|
|
335
475
|
}
|
|
336
|
-
return {
|
|
476
|
+
return { .../** @type {Record<string, unknown>} */ (filters) };
|
|
337
477
|
}
|