@zeyos/zx 4.1.1 → 4.3.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 +81 -0
- package/DESIGN-SYSTEM.md +33 -3
- package/README.md +22 -5
- package/dist/types/components/app-icon/app-icon.d.ts +8 -1
- package/dist/types/components/aurora/aurora.d.ts +72 -0
- package/dist/types/components/calendar/calendar-model.d.ts +202 -0
- package/dist/types/components/calendar/calendar.d.ts +517 -0
- package/dist/types/components/card-view/card-view.d.ts +303 -0
- package/dist/types/components/card-view/record-card.d.ts +242 -0
- package/dist/types/components/dialog/dialog.d.ts +6 -1
- package/dist/types/components/dropdown/dropdown.d.ts +5 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
- package/dist/types/components/launcher/launcher.d.ts +5 -0
- package/dist/types/components/message/message.d.ts +9 -1
- package/dist/types/components/modal/modal.d.ts +44 -7
- package/dist/types/components/sheet/sheet.d.ts +22 -12
- package/dist/types/components/table/table.d.ts +120 -31
- package/dist/types/components/table-view/table-view.d.ts +192 -0
- package/dist/types/components/view/record-view.d.ts +464 -0
- package/dist/types/core/overlay-host.d.ts +12 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/zeyos/calendar.d.ts +91 -0
- package/dist/types/zeyos/icons.d.ts +18 -2
- package/dist/types/zeyos/index.d.ts +4 -0
- package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
- package/dist/types/zeyos/saved-views.d.ts +331 -0
- package/dist/types/zeyos/view.d.ts +305 -0
- package/dist/zx-zeyos.esm.js +4742 -1470
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2536 -471
- package/dist/zx.esm.js +5193 -223
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +5193 -223
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1729 -182
- package/docs/llms.md +236 -32
- package/docs/llms.txt +16 -6
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +1 -1
- package/src/components/app-icon/app-icon.css +77 -51
- package/src/components/app-icon/app-icon.js +28 -4
- package/src/components/app-rail/app-rail.css +12 -3
- package/src/components/app-sidebar/app-sidebar.css +11 -2
- package/src/components/aurora/aurora.css +143 -0
- package/src/components/aurora/aurora.js +146 -0
- package/src/components/avatar/avatar.css +1 -1
- package/src/components/button/button.css +2 -2
- package/src/components/calendar/calendar-model.js +402 -0
- package/src/components/calendar/calendar.css +609 -0
- package/src/components/calendar/calendar.js +1619 -0
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +389 -0
- package/src/components/card-view/card-view.js +527 -0
- package/src/components/card-view/record-card.js +392 -0
- package/src/components/context-menu/context-menu.css +4 -4
- package/src/components/context-menu/context-menu.js +5 -1
- package/src/components/date-picker/date-picker.css +31 -12
- package/src/components/datebox/date-range-box.css +17 -2
- package/src/components/datebox/datebox.css +18 -3
- package/src/components/dialog/dialog.css +4 -3
- package/src/components/dialog/dialog.js +2 -1
- package/src/components/dropdown/dropdown.css +17 -3
- package/src/components/dropdown/dropdown.js +7 -2
- package/src/components/kanban-view/kanban-view.css +216 -0
- package/src/components/kanban-view/kanban-view.js +1228 -0
- package/src/components/launcher/launcher.css +24 -9
- package/src/components/launcher/launcher.js +9 -2
- package/src/components/master-panel/master-panel.css +14 -3
- package/src/components/menu-button/menu-button.css +4 -4
- package/src/components/message/message.css +13 -21
- package/src/components/message/message.js +17 -11
- package/src/components/modal/modal.css +19 -5
- package/src/components/modal/modal.js +99 -25
- package/src/components/navigation-bar/navigation-bar.css +17 -5
- package/src/components/panel/panel.css +10 -2
- package/src/components/select/select.css +26 -8
- package/src/components/sheet/sheet.css +28 -7
- package/src/components/sheet/sheet.js +43 -23
- package/src/components/table/table.css +83 -5
- package/src/components/table/table.js +322 -37
- package/src/components/table-view/table-view.css +18 -0
- package/src/components/table-view/table-view.js +347 -0
- package/src/components/tag-picker/tag-picker.css +18 -4
- package/src/components/tooltip/tooltip.css +17 -3
- package/src/components/tooltip/tooltip.js +4 -1
- package/src/components/view/record-view.css +114 -0
- package/src/components/view/record-view.js +770 -0
- package/src/core/overlay-host.js +23 -0
- package/src/index.js +15 -0
- package/src/zeyos/calendar.js +115 -0
- package/src/zeyos/icons.js +11 -4
- package/src/zeyos/index.js +13 -0
- package/src/zeyos/launcher.js +2 -2
- package/src/zeyos/legacy-saved-views.js +155 -0
- package/src/zeyos/saved-views.js +815 -0
- package/src/zeyos/table.js +3 -1
- package/src/zeyos/view.js +543 -0
- package/styles/icons.css +3 -10
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +142 -3
- package/styles/zx.css +6 -0
package/src/zeyos/table.js
CHANGED
|
@@ -196,7 +196,9 @@ export function zeyosTable(client, resource, opts = {}) {
|
|
|
196
196
|
};
|
|
197
197
|
|
|
198
198
|
table.on('sort', (event) => {
|
|
199
|
-
|
|
199
|
+
const id = event.detail?.id;
|
|
200
|
+
currentSort = id == null ? null
|
|
201
|
+
: { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
|
|
200
202
|
void load({ page: 0 }).catch(() => { /* The connection facade already reported the failure. */ });
|
|
201
203
|
});
|
|
202
204
|
return binding;
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { buildListQuery } from './query.js';
|
|
2
|
+
import { connect } from './connect.js';
|
|
3
|
+
import { normalizeZeyosListResult } from './select.js';
|
|
4
|
+
import { createSavedViewRegistry } from './saved-views.js';
|
|
5
|
+
import { buildZeyosTableConfig } from './table.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../components/view/record-view.js').ViewField} ViewField */
|
|
8
|
+
/** @typedef {import('../components/view/record-view.js').ViewSort} ViewSort */
|
|
9
|
+
/** @typedef {import('./saved-views.js').SavedViewEntry} SavedViewEntry */
|
|
10
|
+
/** @typedef {import('./saved-views.js').SavedViewApplyResult} SavedViewApplyResult */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ZeyosSavedViewsOptions
|
|
14
|
+
* @property {Record<string,any>} [controller] Existing saved-view registry. It remains caller-owned.
|
|
15
|
+
* @property {Record<string,any>} [registry] Alias for `controller`.
|
|
16
|
+
* @property {Record<string,any>} [transport] Injected persistence transport used to construct a registry.
|
|
17
|
+
* @property {{userId:string|number,workspaceId:string|number|null,resource?:string}} [scope]
|
|
18
|
+
* Saved-view isolation scope; null workspace identifies ZeyOS's base workspace.
|
|
19
|
+
* resource defaults to the bound ZeyOS resource.
|
|
20
|
+
* @property {Record<string,unknown>} [options] Options passed to `createSavedViewRegistry`.
|
|
21
|
+
* @property {'table'|'card'|'kanban'} [type] Presentation identity; normally derived from the View.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} ZeyosViewOptions
|
|
26
|
+
* @property {string[]} [fields] Curated ordered schema field allow-list.
|
|
27
|
+
* @property {string[]} [exclude] Schema fields to omit.
|
|
28
|
+
* @property {Record<string, string>} [labels] Per-field label overrides.
|
|
29
|
+
* @property {Record<string, Partial<ViewField>>} [fieldOverrides] View descriptor overrides.
|
|
30
|
+
* @property {string|((record:Record<string,unknown>)=>unknown)} [recordId='ID'] Record id accessor.
|
|
31
|
+
* @property {number} [pageSize=50] Server page size.
|
|
32
|
+
* @property {ViewSort|null} [sort=null] Initial server sort.
|
|
33
|
+
* @property {Record<string, unknown>} [filters] Initial ZeyOS predicates.
|
|
34
|
+
* @property {string} [search=''] Initial full-text query.
|
|
35
|
+
* @property {string[]} [searchFields] Server full-text fields where supported.
|
|
36
|
+
* @property {Record<string, unknown>} [viewOptions] Options passed to the concrete view.
|
|
37
|
+
* @property {Element|string|null} [target=null] Concrete view target.
|
|
38
|
+
* @property {string} [titleField] Card title field included in the projection.
|
|
39
|
+
* @property {string} [subtitleField] Card subtitle field included in the projection.
|
|
40
|
+
* @property {string|Function} [preview] Preview field or resolver.
|
|
41
|
+
* @property {string|Function} [groupBy] Card grouping field or resolver.
|
|
42
|
+
* @property {string|Function} [columnBy] Kanban column field or resolver.
|
|
43
|
+
* @property {string|Function} [swimlaneBy] Kanban swim-lane field or resolver.
|
|
44
|
+
* @property {false|Record<string,any>|ZeyosSavedViewsOptions|null} [savedViews] Existing registry or
|
|
45
|
+
* injected transport/scope configuration for named saved views.
|
|
46
|
+
* @property {(record:Record<string,unknown>,event:CustomEvent<Record<string,unknown>>)=>void} [onRecordClick]
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} ZeyosViewConfig
|
|
51
|
+
* @property {Record<string, any>[]} fieldMeta Ordered normalized schema metadata.
|
|
52
|
+
* @property {ViewField[]} fields Common view fields.
|
|
53
|
+
* @property {Record<string, string>} projection Complete server projection.
|
|
54
|
+
* @property {Record<string, any>} viewOptions Concrete view options.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} ZeyosViewLoadOptions
|
|
59
|
+
* @property {string} [search] Replacement full-text query.
|
|
60
|
+
* @property {Record<string, unknown>} [filters] Replacement predicates.
|
|
61
|
+
* @property {number} [page=0] Zero-based page; pages above zero append.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Object} ZeyosViewLoadResult
|
|
66
|
+
* @property {Record<string, unknown>[]} data Loaded records.
|
|
67
|
+
* @property {number|null} count Server count when supplied.
|
|
68
|
+
* @property {number} page Loaded page.
|
|
69
|
+
* @property {boolean} hasMore Whether another page is available.
|
|
70
|
+
* @property {Record<string, unknown>} query Request sent to the list operation.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {Object} ZeyosSavedViewCapture
|
|
75
|
+
* @property {string} [id] Existing saved-view id.
|
|
76
|
+
* @property {string} name Saved-view name.
|
|
77
|
+
* @property {'table'|'card'|'kanban'} type Concrete presentation.
|
|
78
|
+
* @property {Record<string,unknown>} state JSON-safe component view state.
|
|
79
|
+
* @property {Record<string,unknown>} filters Current server filters.
|
|
80
|
+
* @property {string} search Current full-text query.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {Object} ZeyosSaveViewOptions
|
|
85
|
+
* @property {string} [id] Existing saved-view id to replace.
|
|
86
|
+
* @property {string} name Saved-view name.
|
|
87
|
+
* @property {'reject'|'replace'} [onDuplicate] Duplicate-name behavior.
|
|
88
|
+
* @property {boolean} [setDefault=false] Make the saved entry the default.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {Object} ZeyosApplyViewOptions
|
|
93
|
+
* @property {boolean} [load=true] Reload the first server page after an applied view.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @typedef {Object} ZeyosViewBinding
|
|
98
|
+
* @property {Record<string, any>} view Generated RecordView-compatible instance.
|
|
99
|
+
* @property {(options?:ZeyosViewLoadOptions)=>Promise<ZeyosViewLoadResult>} load Loads a page.
|
|
100
|
+
* @property {(search:string)=>Promise<ZeyosViewLoadResult>} setSearch Replaces search and reloads.
|
|
101
|
+
* @property {(filters:Record<string,unknown>)=>Promise<ZeyosViewLoadResult>} setFilters Replaces filters and reloads.
|
|
102
|
+
* @property {()=>Promise<ZeyosViewLoadResult>} loadMore Loads the next page when available.
|
|
103
|
+
* @property {number|null} count Current server count.
|
|
104
|
+
* @property {number} page Current zero-based page.
|
|
105
|
+
* @property {boolean} hasMore Whether another page is available.
|
|
106
|
+
* @property {Record<string,any>|null} savedViews Saved-view registry when configured.
|
|
107
|
+
* @property {Promise<SavedViewApplyResult|null>} ready Default restoration completed before loads.
|
|
108
|
+
* @property {(options?:ZeyosApplyViewOptions)=>Promise<SavedViewApplyResult|null>} restoreDefault Restores the current default.
|
|
109
|
+
* @property {(details:{id?:string,name:string})=>ZeyosSavedViewCapture} captureView Captures current query/view state without persistence.
|
|
110
|
+
* @property {(details:ZeyosSaveViewOptions)=>Promise<SavedViewEntry>} saveView Captures and persists the current view.
|
|
111
|
+
* @property {(idOrName:string,options?:ZeyosApplyViewOptions)=>Promise<SavedViewApplyResult>} applyView Applies a compatible named view.
|
|
112
|
+
* @property {()=>void} destroy Destroys the generated view.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/** View-specific top-level conveniences copied into the concrete component options. */
|
|
116
|
+
const VIEW_KEYS = [
|
|
117
|
+
'titleField', 'subtitleField', 'preview', 'previewAlt', 'link', 'actions', 'groupBy', 'groupOrder',
|
|
118
|
+
'columnBy', 'columns', 'swimlaneBy', 'swimlanes', 'moveMode', 'columnOrder', 'swimlaneOrder',
|
|
119
|
+
'collapsedColumns', 'collapsedSwimlanes', 'showCounts', 'showEmptyColumns', 'hiddenFields',
|
|
120
|
+
'fieldOrder', 'fieldControls', 'selectable', 'selection', 'emptyText'
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Produces common view fields and a complete ZeyOS projection without touching the DOM. It reuses
|
|
125
|
+
* the established table schema conversion so entity labels, enums, amounts, currencies, and Unix
|
|
126
|
+
* dates render identically in TableView, CardView, and KanbanView.
|
|
127
|
+
* @param {Record<string, any>} client Injected ZeyOS client.
|
|
128
|
+
* @param {string} resource Schema resource.
|
|
129
|
+
* @param {ZeyosViewOptions} [opts={}] Schema and view options.
|
|
130
|
+
* @returns {ZeyosViewConfig} DOM-free configuration.
|
|
131
|
+
*/
|
|
132
|
+
export function buildZeyosViewConfig(client, resource, opts = {}) {
|
|
133
|
+
const requested = requestedViewFields(client, resource, opts);
|
|
134
|
+
const table = buildZeyosTableConfig(client, resource, {
|
|
135
|
+
fields: requested,
|
|
136
|
+
labels: opts.labels,
|
|
137
|
+
rowId: opts.recordId ?? 'ID',
|
|
138
|
+
sort: opts.sort ?? null
|
|
139
|
+
});
|
|
140
|
+
const overrides = opts.fieldOverrides && typeof opts.fieldOverrides === 'object'
|
|
141
|
+
? opts.fieldOverrides : {};
|
|
142
|
+
const explicitlyVisible = Array.isArray(opts.fields) ? new Set(opts.fields.map(String)) : null;
|
|
143
|
+
const fields = table.columns.map((column) => ({
|
|
144
|
+
...column,
|
|
145
|
+
...(explicitlyVisible && !explicitlyVisible.has(column.id) ? { visible: false } : {}),
|
|
146
|
+
...(overrides[column.id] ?? {}),
|
|
147
|
+
id: column.id
|
|
148
|
+
}));
|
|
149
|
+
const projection = { ...table.projection };
|
|
150
|
+
|
|
151
|
+
// A view resolver may name a schema field outside the visible field allow-list. The server still
|
|
152
|
+
// has to return it for grouping/media. Direct projections are safe even for compatible client
|
|
153
|
+
// schemas whose field metadata omitted a virtual field.
|
|
154
|
+
for (const id of supplementalFieldIds(opts)) {
|
|
155
|
+
if (!Object.prototype.hasOwnProperty.call(projection, id)) projection[id] = id;
|
|
156
|
+
}
|
|
157
|
+
const viewOptions = { ...(opts.viewOptions ?? {}) };
|
|
158
|
+
for (const key of VIEW_KEYS) {
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(opts, key)) viewOptions[key] = opts[key];
|
|
160
|
+
}
|
|
161
|
+
Object.assign(viewOptions, {
|
|
162
|
+
fields,
|
|
163
|
+
data: [],
|
|
164
|
+
recordId: opts.recordId ?? 'ID',
|
|
165
|
+
sort: opts.sort ?? null,
|
|
166
|
+
sortMode: 'server'
|
|
167
|
+
});
|
|
168
|
+
if (typeof opts.onRecordClick === 'function') {
|
|
169
|
+
viewOptions.onrecordclick = (event) => opts.onRecordClick(event.detail.record, event);
|
|
170
|
+
}
|
|
171
|
+
return { fieldMeta: table.fieldMeta, fields, projection, viewOptions };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates and binds any RecordView-compatible constructor to server paging. The caller supplies the
|
|
176
|
+
* constructor, so this adapter has no dependency on one concrete presentation and remains simple
|
|
177
|
+
* to unit test or wrap from a framework.
|
|
178
|
+
* @param {Record<string, any>} client Injected ZeyOS client.
|
|
179
|
+
* @param {string} resource Schema resource.
|
|
180
|
+
* @param {new(target:Element|string|null,options:Record<string,unknown>)=>Record<string,any>} View Concrete view class.
|
|
181
|
+
* @param {ZeyosViewOptions} [opts={}] Binding options.
|
|
182
|
+
* @returns {ZeyosViewBinding} View and server-state methods.
|
|
183
|
+
*/
|
|
184
|
+
export function zeyosView(client, resource, View, opts = {}) {
|
|
185
|
+
if (typeof View !== 'function') throw new TypeError('zeyosView() requires a view constructor');
|
|
186
|
+
const config = buildZeyosViewConfig(client, resource, opts);
|
|
187
|
+
const view = new View(opts.target ?? null, config.viewOptions);
|
|
188
|
+
for (const method of ['setData', 'addData', 'setLoading', 'on', 'destroy']) {
|
|
189
|
+
if (typeof view?.[method] !== 'function') {
|
|
190
|
+
try { view?.destroy?.(); } catch { /* Preserve the contract error. */ }
|
|
191
|
+
throw new TypeError(`zeyosView() constructor must provide ${method}()`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const facade = connect(client);
|
|
195
|
+
const pageSize = normalizePageSize(opts.pageSize);
|
|
196
|
+
let saved;
|
|
197
|
+
try {
|
|
198
|
+
saved = resolveSavedViews(opts.savedViews, View, resource);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
view.destroy();
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
const savedViews = saved?.registry ?? null;
|
|
204
|
+
const presentation = saved?.type ?? presentationType(View);
|
|
205
|
+
if (savedViews) {
|
|
206
|
+
for (const method of ['getViewState', 'setViewState']) {
|
|
207
|
+
if (typeof view?.[method] !== 'function') {
|
|
208
|
+
if (saved?.owned) savedViews.destroy?.();
|
|
209
|
+
view.destroy();
|
|
210
|
+
throw new TypeError(`zeyosView() saved views require ${method}()`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let currentSort = opts.sort ?? null;
|
|
215
|
+
let currentFilters = copyFilters(opts.filters ?? {});
|
|
216
|
+
let currentSearch = String(opts.search ?? '');
|
|
217
|
+
let currentPage = 0;
|
|
218
|
+
let totalCount = null;
|
|
219
|
+
let more = false;
|
|
220
|
+
let requestSequence = 0;
|
|
221
|
+
let destroyed = false;
|
|
222
|
+
|
|
223
|
+
const performLoad = async (options = {}) => {
|
|
224
|
+
if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
|
|
225
|
+
if (Object.prototype.hasOwnProperty.call(options, 'search')) currentSearch = String(options.search ?? '');
|
|
226
|
+
if (Object.prototype.hasOwnProperty.call(options, 'filters')) currentFilters = copyFilters(options.filters ?? {});
|
|
227
|
+
const page = normalizePage(options.page ?? 0);
|
|
228
|
+
const query = buildListQuery({
|
|
229
|
+
fields: config.projection,
|
|
230
|
+
sort: currentSort,
|
|
231
|
+
filters: currentFilters,
|
|
232
|
+
search: currentSearch,
|
|
233
|
+
searchFields: opts.searchFields,
|
|
234
|
+
limit: pageSize,
|
|
235
|
+
offset: page * pageSize,
|
|
236
|
+
count: true
|
|
237
|
+
});
|
|
238
|
+
const sequence = ++requestSequence;
|
|
239
|
+
view.setLoading(true);
|
|
240
|
+
try {
|
|
241
|
+
const normalized = normalizeZeyosListResult(client, await facade.list(resource, query));
|
|
242
|
+
const count = normalized.count ?? null;
|
|
243
|
+
const hasMore = count == null
|
|
244
|
+
? normalized.data.length === pageSize
|
|
245
|
+
: page * pageSize + normalized.data.length < count;
|
|
246
|
+
if (!destroyed && sequence === requestSequence) {
|
|
247
|
+
currentPage = page;
|
|
248
|
+
totalCount = count;
|
|
249
|
+
more = hasMore;
|
|
250
|
+
if (page === 0) view.setData(normalized.data);
|
|
251
|
+
else view.addData(normalized.data);
|
|
252
|
+
}
|
|
253
|
+
return { data: normalized.data, count, page, hasMore, query };
|
|
254
|
+
} finally {
|
|
255
|
+
if (!destroyed && sequence === requestSequence) view.setLoading(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** @param {string} idOrName @param {{load?:boolean}} [options={}] @returns {Promise<SavedViewApplyResult>} */
|
|
260
|
+
const applySaved = async (idOrName, options = {}) => {
|
|
261
|
+
if (!savedViews || !presentation) throw new Error('This ZeyOS view binding has no saved-view registry');
|
|
262
|
+
if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
|
|
263
|
+
const result = await savedViews.apply(idOrName, view, {
|
|
264
|
+
type: presentation,
|
|
265
|
+
onQuery: async ({ filters, search }) => {
|
|
266
|
+
if (destroyed) return;
|
|
267
|
+
currentFilters = copyFilters(filters ?? {});
|
|
268
|
+
currentSearch = String(search ?? '');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
if (!result.applied || destroyed) return result;
|
|
272
|
+
currentSort = appliedViewSort(view, result.entry);
|
|
273
|
+
currentPage = 0;
|
|
274
|
+
totalCount = null;
|
|
275
|
+
more = false;
|
|
276
|
+
if (options.load !== false) await performLoad({ page: 0 });
|
|
277
|
+
return result;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** @param {{load?:boolean}} [options={}] @returns {Promise<SavedViewApplyResult|null>} */
|
|
281
|
+
const restoreDefaultInternal = async (options = {}) => {
|
|
282
|
+
if (!savedViews) return null;
|
|
283
|
+
await readySavedViews(savedViews);
|
|
284
|
+
if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
|
|
285
|
+
const entry = await savedViews.getDefault();
|
|
286
|
+
if (!entry) return /** @type {SavedViewApplyResult} */ ({
|
|
287
|
+
status: 'missing', applied: false, entry: null
|
|
288
|
+
});
|
|
289
|
+
return applySaved(entry.id, options);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** Default application starts immediately and every server load below awaits it. */
|
|
293
|
+
const ready = savedViews ? restoreDefaultInternal({ load: false }) : Promise.resolve(null);
|
|
294
|
+
let initialized = !savedViews;
|
|
295
|
+
let retryingInitialization = null;
|
|
296
|
+
// The promise remains rejectable to callers/load(), while this observer prevents an ignored
|
|
297
|
+
// optional saved-view configuration from becoming a process-level unhandled rejection.
|
|
298
|
+
void ready.then(() => { initialized = true; }, () => {});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Later operations retry a failed initial registry/default restoration instead of remaining
|
|
302
|
+
* poisoned by the intentionally observable first `ready` rejection.
|
|
303
|
+
* @param {{load?:boolean}} [options={load:false}] Retry apply behavior.
|
|
304
|
+
* @returns {Promise<SavedViewApplyResult|null>}
|
|
305
|
+
*/
|
|
306
|
+
const ensureInitialized = async (options = { load: false }) => {
|
|
307
|
+
if (initialized) return null;
|
|
308
|
+
try {
|
|
309
|
+
const result = await ready;
|
|
310
|
+
initialized = true;
|
|
311
|
+
return result;
|
|
312
|
+
} catch {
|
|
313
|
+
// The first failure remains available through binding.ready; this operation gets a retry.
|
|
314
|
+
}
|
|
315
|
+
if (!retryingInitialization) {
|
|
316
|
+
const retry = restoreDefaultInternal(options).then((result) => {
|
|
317
|
+
initialized = true;
|
|
318
|
+
return result;
|
|
319
|
+
});
|
|
320
|
+
const pending = retry.finally(() => {
|
|
321
|
+
if (retryingInitialization === pending) retryingInitialization = null;
|
|
322
|
+
});
|
|
323
|
+
retryingInitialization = pending;
|
|
324
|
+
}
|
|
325
|
+
return retryingInitialization;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const load = async (options = {}) => {
|
|
329
|
+
await ensureInitialized({ load: false });
|
|
330
|
+
return performLoad(options);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const captureView = (details) => {
|
|
334
|
+
if (!presentation || typeof view.getViewState !== 'function') {
|
|
335
|
+
throw new Error('This view does not expose serializable saved-view state');
|
|
336
|
+
}
|
|
337
|
+
const source = details && typeof details === 'object' ? details : {};
|
|
338
|
+
return {
|
|
339
|
+
...(source.id == null ? {} : { id: String(source.id) }),
|
|
340
|
+
name: String(source.name ?? ''),
|
|
341
|
+
type: presentation,
|
|
342
|
+
state: view.getViewState(),
|
|
343
|
+
filters: copyFilters(currentFilters),
|
|
344
|
+
search: currentSearch
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const saveView = async (details) => {
|
|
349
|
+
if (!savedViews || !presentation) throw new Error('This ZeyOS view binding has no saved-view registry');
|
|
350
|
+
await ensureInitialized({ load: false });
|
|
351
|
+
if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
|
|
352
|
+
const capture = captureView(details);
|
|
353
|
+
const entry = await savedViews.capture(view, {
|
|
354
|
+
...(capture.id == null ? {} : { id: capture.id }),
|
|
355
|
+
name: capture.name,
|
|
356
|
+
type: presentation,
|
|
357
|
+
filters: capture.filters,
|
|
358
|
+
search: capture.search
|
|
359
|
+
}, { onDuplicate: details?.onDuplicate, setDefault: Boolean(details?.setDefault) });
|
|
360
|
+
return entry;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const applyView = async (idOrName, options = {}) => {
|
|
364
|
+
await ensureInitialized({ load: false });
|
|
365
|
+
return applySaved(idOrName, options);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const restoreDefault = async (options = {}) => {
|
|
369
|
+
if (!initialized) return ensureInitialized(options);
|
|
370
|
+
return restoreDefaultInternal(options);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const sortHandler = (event) => {
|
|
374
|
+
const id = event.detail?.id;
|
|
375
|
+
currentSort = id == null ? null : { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
|
|
376
|
+
void load({ page: 0 }).catch(() => { /* connect() already reported the API failure. */ });
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const binding = {
|
|
380
|
+
view,
|
|
381
|
+
load,
|
|
382
|
+
setSearch: (search) => load({ search, page: 0 }),
|
|
383
|
+
setFilters: (filters) => load({ filters, page: 0 }),
|
|
384
|
+
loadMore: async () => {
|
|
385
|
+
await ensureInitialized({ load: false });
|
|
386
|
+
return more ? performLoad({ page: currentPage + 1 }) : {
|
|
387
|
+
data: [],
|
|
388
|
+
count: totalCount,
|
|
389
|
+
page: currentPage,
|
|
390
|
+
hasMore: false,
|
|
391
|
+
query: buildListQuery({
|
|
392
|
+
fields: config.projection,
|
|
393
|
+
sort: currentSort,
|
|
394
|
+
filters: currentFilters,
|
|
395
|
+
search: currentSearch,
|
|
396
|
+
searchFields: opts.searchFields,
|
|
397
|
+
limit: pageSize,
|
|
398
|
+
offset: currentPage * pageSize,
|
|
399
|
+
count: true
|
|
400
|
+
})
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
get count() { return totalCount; },
|
|
404
|
+
get page() { return currentPage; },
|
|
405
|
+
get hasMore() { return more; },
|
|
406
|
+
savedViews,
|
|
407
|
+
ready,
|
|
408
|
+
restoreDefault,
|
|
409
|
+
captureView,
|
|
410
|
+
saveView,
|
|
411
|
+
applyView,
|
|
412
|
+
destroy: () => {
|
|
413
|
+
if (destroyed) return;
|
|
414
|
+
destroyed = true;
|
|
415
|
+
requestSequence += 1;
|
|
416
|
+
if (typeof view.off === 'function') view.off('sortchange', sortHandler);
|
|
417
|
+
else view.removeEventListener?.('sortchange', sortHandler);
|
|
418
|
+
if (saved?.owned) savedViews?.destroy?.();
|
|
419
|
+
view.destroy();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
view.on('sortchange', sortHandler);
|
|
423
|
+
return binding;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @param {Record<string,any>} client @param {string} resource @param {ZeyosViewOptions} opts @returns {string[]|undefined} */
|
|
427
|
+
function requestedViewFields(client, resource, opts) {
|
|
428
|
+
if (!Array.isArray(opts.fields) && !Array.isArray(opts.exclude)) return undefined;
|
|
429
|
+
const excluded = new Set(Array.isArray(opts.exclude) ? opts.exclude.map(String) : []);
|
|
430
|
+
const listed = Array.isArray(opts.fields) ? opts.fields.map(String) : schemaFieldIds(client, resource);
|
|
431
|
+
return [...new Set([...listed, ...supplementalFieldIds(opts)])]
|
|
432
|
+
.filter((id) => !excluded.has(id));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** @param {Record<string,any>} client @param {string} resource @returns {string[]} */
|
|
436
|
+
function schemaFieldIds(client, resource) {
|
|
437
|
+
const collection = client?.schema?.fields?.(resource);
|
|
438
|
+
if (Array.isArray(collection)) return collection.map((entry) => typeof entry === 'string'
|
|
439
|
+
? entry : String(entry?.id ?? entry?.name ?? entry?.field ?? '')).filter(Boolean);
|
|
440
|
+
return collection && typeof collection === 'object' ? Object.keys(collection) : [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** @param {ZeyosViewOptions} opts @returns {string[]} */
|
|
444
|
+
function supplementalFieldIds(opts) {
|
|
445
|
+
const ids = [];
|
|
446
|
+
for (const key of ['titleField', 'subtitleField', 'preview', 'groupBy', 'columnBy', 'swimlaneBy']) {
|
|
447
|
+
const value = opts[key];
|
|
448
|
+
if (typeof value === 'string' && value.trim()) ids.push(value.trim());
|
|
449
|
+
}
|
|
450
|
+
return [...new Set(ids)];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** @param {unknown} value @returns {number} */
|
|
454
|
+
function normalizePageSize(value) {
|
|
455
|
+
const number = Number(value ?? 50);
|
|
456
|
+
return Number.isFinite(number) ? Math.max(1, Math.floor(number)) : 50;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** @param {unknown} value @returns {number} */
|
|
460
|
+
function normalizePage(value) {
|
|
461
|
+
const number = Number(value);
|
|
462
|
+
return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @param {unknown} value @returns {Record<string, unknown>} */
|
|
466
|
+
function copyFilters(value) {
|
|
467
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Resolves a supplied registry or constructs one from an injected transport and scope.
|
|
472
|
+
* @param {ZeyosViewOptions['savedViews']} value Saved-view option.
|
|
473
|
+
* @param {Function & {cssName?:string}} View Concrete view constructor.
|
|
474
|
+
* @param {string} resource ZeyOS resource.
|
|
475
|
+
* @returns {{registry:Record<string,any>,type:'table'|'card'|'kanban',owned:boolean}|null}
|
|
476
|
+
*/
|
|
477
|
+
function resolveSavedViews(value, View, resource) {
|
|
478
|
+
if (value == null || value === false) return null;
|
|
479
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
480
|
+
throw new TypeError('savedViews must be a registry or transport/scope configuration');
|
|
481
|
+
}
|
|
482
|
+
const source = /** @type {Record<string,any>} */ (value);
|
|
483
|
+
const configured = source.controller ?? source.registry;
|
|
484
|
+
const direct = isSavedViewRegistry(source) ? source : isSavedViewRegistry(configured) ? configured : null;
|
|
485
|
+
const type = normalizePresentationType(source.type ?? presentationType(View));
|
|
486
|
+
if (!type) throw new TypeError('savedViews requires a table, card, or kanban presentation type');
|
|
487
|
+
if (direct) return { registry: direct, type, owned: false };
|
|
488
|
+
if (!source.transport || !source.scope || typeof source.scope !== 'object') {
|
|
489
|
+
throw new TypeError('savedViews requires a controller or injected transport and scope');
|
|
490
|
+
}
|
|
491
|
+
const scope = { ...source.scope, resource: source.scope.resource ?? resource };
|
|
492
|
+
return {
|
|
493
|
+
registry: createSavedViewRegistry(source.transport, scope, source.options ?? {}),
|
|
494
|
+
type,
|
|
495
|
+
owned: true
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** @param {unknown} value @returns {boolean} */
|
|
500
|
+
function isSavedViewRegistry(value) {
|
|
501
|
+
return Boolean(value && typeof value === 'object'
|
|
502
|
+
&& typeof /** @type {Record<string,any>} */ (value).apply === 'function'
|
|
503
|
+
&& typeof /** @type {Record<string,any>} */ (value).capture === 'function'
|
|
504
|
+
&& typeof /** @type {Record<string,any>} */ (value).getDefault === 'function');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** @param {Function & {cssName?:string}} View @returns {'table'|'card'|'kanban'|null} */
|
|
508
|
+
function presentationType(View) {
|
|
509
|
+
const cssName = String(View.cssName ?? '').toLowerCase().replace(/-view$/, '');
|
|
510
|
+
if (['table', 'card', 'kanban'].includes(cssName)) {
|
|
511
|
+
return /** @type {'table'|'card'|'kanban'} */ (cssName);
|
|
512
|
+
}
|
|
513
|
+
const name = String(View.name ?? '').toLowerCase().replace(/view$/, '');
|
|
514
|
+
return ['table', 'card', 'kanban'].includes(name)
|
|
515
|
+
? /** @type {'table'|'card'|'kanban'} */ (name) : null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** @param {unknown} value @returns {'table'|'card'|'kanban'|null} */
|
|
519
|
+
function normalizePresentationType(value) {
|
|
520
|
+
const type = String(value ?? '').toLowerCase();
|
|
521
|
+
return ['table', 'card', 'kanban'].includes(type)
|
|
522
|
+
? /** @type {'table'|'card'|'kanban'} */ (type) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** @param {Record<string,any>} registry @returns {Promise<void>} */
|
|
526
|
+
async function readySavedViews(registry) {
|
|
527
|
+
if (typeof registry.ready === 'function') await registry.ready();
|
|
528
|
+
else if (typeof registry.load === 'function') await registry.load();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Reads the sort after `setViewState()` so unknown saved fields rejected by RecordView do not leak
|
|
533
|
+
* into the server query. The entry is only a fallback for compatible facade implementations.
|
|
534
|
+
* @param {Record<string,any>} view Applied view.
|
|
535
|
+
* @param {SavedViewEntry|null|undefined} entry Saved entry.
|
|
536
|
+
* @returns {ViewSort|null}
|
|
537
|
+
*/
|
|
538
|
+
function appliedViewSort(view, entry) {
|
|
539
|
+
const sort = typeof view.getSort === 'function' ? view.getSort()
|
|
540
|
+
: typeof view.getViewState === 'function' ? view.getViewState()?.sort : entry?.state?.sort;
|
|
541
|
+
if (!sort || typeof sort !== 'object' || Array.isArray(sort) || sort.id == null) return null;
|
|
542
|
+
return { id: String(sort.id), dir: sort.dir === 'desc' ? 'desc' : 'asc' };
|
|
543
|
+
}
|
package/styles/icons.css
CHANGED
|
@@ -19,16 +19,9 @@ svg.zx-icon {
|
|
|
19
19
|
vertical-align: middle;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/* ZeyOS
|
|
23
|
-
|
|
22
|
+
/* ZeyOS identity hook. `moduleChip()` now delegates vessel geometry and material entirely to
|
|
23
|
+
AppIcon; this class only preserves the module variables for host styling and older selectors. */
|
|
24
24
|
.zx-module-icon {
|
|
25
|
-
display: inline-grid;
|
|
26
|
-
place-items: center;
|
|
27
25
|
flex: none;
|
|
28
|
-
|
|
29
|
-
block-size: var(--zx-module-icon-size, 1.5rem);
|
|
30
|
-
border-radius: var(--zx-radius-sm);
|
|
31
|
-
background: var(--zx-module-color, var(--zx-module-default));
|
|
32
|
-
color: var(--zx-module-glyph, var(--zx-module-fg-light));
|
|
33
|
-
font-size: calc(var(--zx-module-icon-size, 1.5rem) * 0.6);
|
|
26
|
+
color: var(--zx-module-glyph, var(--zx-color-app-icon-glyph));
|
|
34
27
|
}
|
package/styles/tokens/dark.css
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
--zx-color-bg-hover: color-mix(in srgb, var(--zx-gray-0) 8%, transparent);
|
|
20
20
|
--zx-color-bg-selected: color-mix(in srgb, var(--zx-accent-400) 18%, var(--zx-gray-900));
|
|
21
21
|
--zx-color-bg-backdrop: color-mix(in srgb, var(--zx-gray-950) 75%, transparent);
|
|
22
|
+
--zx-color-overlay-base: color-mix(in srgb, var(--zx-color-bg-raised) 42%, var(--zx-color-bg-page));
|
|
23
|
+
--zx-color-overlay-backdrop: color-mix(in srgb, var(--zx-gray-950) 38%, transparent);
|
|
22
24
|
--zx-color-border: color-mix(in srgb, var(--zx-gray-0) 10%, transparent);
|
|
23
25
|
--zx-color-border-strong: color-mix(in srgb, var(--zx-gray-0) 28%, transparent);
|
|
24
26
|
--zx-color-border-control: color-mix(in srgb, var(--zx-gray-0) 15%, transparent);
|
|
@@ -57,6 +59,8 @@
|
|
|
57
59
|
--zx-color-bg-hover: color-mix(in srgb, var(--zx-gray-0) 8%, transparent);
|
|
58
60
|
--zx-color-bg-selected: color-mix(in srgb, var(--zx-accent-400) 18%, var(--zx-gray-900));
|
|
59
61
|
--zx-color-bg-backdrop: color-mix(in srgb, var(--zx-gray-950) 75%, transparent);
|
|
62
|
+
--zx-color-overlay-base: color-mix(in srgb, var(--zx-color-bg-raised) 42%, var(--zx-color-bg-page));
|
|
63
|
+
--zx-color-overlay-backdrop: color-mix(in srgb, var(--zx-gray-950) 38%, transparent);
|
|
60
64
|
--zx-color-border: color-mix(in srgb, var(--zx-gray-0) 10%, transparent);
|
|
61
65
|
--zx-color-border-strong: color-mix(in srgb, var(--zx-gray-0) 28%, transparent);
|
|
62
66
|
--zx-color-border-control: color-mix(in srgb, var(--zx-gray-0) 15%, transparent);
|