@object-ui/core 17.3.0 → 17.5.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +1224 -0
  2. package/README.md +2 -12
  3. package/dist/actions/ActionEngine.d.ts +42 -25
  4. package/dist/actions/ActionEngine.js +66 -52
  5. package/dist/actions/ActionRunner.d.ts +199 -21
  6. package/dist/actions/ActionRunner.js +332 -16
  7. package/dist/actions/actionKeys.d.ts +96 -20
  8. package/dist/actions/actionKeys.js +157 -18
  9. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  10. package/dist/data-scope/ViewDataProvider.js +83 -5
  11. package/dist/data-scope/element-data-source.d.ts +113 -0
  12. package/dist/data-scope/element-data-source.js +216 -0
  13. package/dist/data-scope/index.d.ts +2 -1
  14. package/dist/data-scope/index.js +1 -0
  15. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  16. package/dist/evaluator/declaredPredicate.js +152 -0
  17. package/dist/evaluator/fieldRules.d.ts +16 -0
  18. package/dist/evaluator/fieldRules.js +51 -16
  19. package/dist/evaluator/index.d.ts +1 -0
  20. package/dist/evaluator/index.js +1 -0
  21. package/dist/evaluator/listConditional.d.ts +5 -2
  22. package/dist/evaluator/listConditional.js +58 -16
  23. package/dist/evaluator/predicateInput.d.ts +51 -3
  24. package/dist/evaluator/predicateInput.js +62 -5
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.js +14 -1
  27. package/dist/protocols/DndProtocol.d.ts +42 -1
  28. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  29. package/dist/protocols/SharingProtocol.d.ts +11 -1
  30. package/dist/registry/Registry.d.ts +24 -0
  31. package/dist/theme/ThemeEngine.d.ts +0 -8
  32. package/dist/theme/ThemeEngine.js +22 -103
  33. package/dist/theme/index.d.ts +1 -1
  34. package/dist/theme/index.js +7 -1
  35. package/dist/types/index.d.ts +42 -8
  36. package/dist/utils/chart-series.d.ts +258 -5
  37. package/dist/utils/chart-series.js +423 -20
  38. package/dist/utils/compare-to.d.ts +40 -13
  39. package/dist/utils/compare-to.js +4 -32
  40. package/dist/utils/dashboard-filters.d.ts +54 -8
  41. package/dist/utils/dashboard-filters.js +261 -12
  42. package/dist/utils/dataset-format.d.ts +73 -14
  43. package/dist/utils/dataset-format.js +130 -9
  44. package/dist/utils/dataset-pivot.d.ts +28 -7
  45. package/dist/utils/dataset-pivot.js +27 -6
  46. package/dist/utils/dom-props.d.ts +137 -0
  47. package/dist/utils/dom-props.js +153 -0
  48. package/dist/utils/normalize-list-view.d.ts +26 -6
  49. package/dist/utils/normalize-list-view.js +30 -8
  50. package/dist/utils/number-display.d.ts +151 -0
  51. package/dist/utils/number-display.js +71 -0
  52. package/dist/utils/resolve-view-id.d.ts +31 -0
  53. package/dist/utils/resolve-view-id.js +45 -0
  54. package/dist/utils/sort-query.d.ts +55 -0
  55. package/dist/utils/sort-query.js +35 -0
  56. package/package.json +5 -5
  57. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  58. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -6,26 +6,46 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * What an action is allowed to carry — objectstack#4075 step 1.
9
+ * What an action is allowed to carry — objectstack#4075, all three steps.
10
10
  *
11
- * `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
12
- * Concretely (objectui#2990): deleting `ActionDef.execute` produced ZERO compile
13
- * errors even though the field had just been removed, and stale metadata still
14
- * authoring `execute: 'markDone'` type-checks today. The same deletion against
15
- * `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
16
- * produced `TS2353` at the authoring site. One of the two readers can catch a
17
- * retired key; the other is structurally incapable.
11
+ * `ActionDef` used to end with `[key: string]: any`, so it accepted any key of
12
+ * any type. Concretely (objectui#2990): deleting `ActionDef.execute` produced
13
+ * ZERO compile errors even though the field had just been removed, and stale
14
+ * metadata authoring `execute: 'markDone'` type-checked. The same deletion
15
+ * against `@object-ui/types`' `ActionSchema` — which has no index signature —
16
+ * correctly produced `TS2353` at the authoring site. One of the two readers
17
+ * could catch a retired key; the other was structurally incapable.
18
18
  *
19
- * That asymmetry is the issue. An open key set on a DECLARED METADATA CONTRACT
19
+ * That asymmetry was the issue. An open key set on a DECLARED METADATA CONTRACT
20
20
  * is what lets a typo (`targt`, `exectue`) and a tombstoned key (`execute`) both
21
21
  * sail through to a runner that then silently does nothing — the #2169 "Mark
22
22
  * Done does nothing" shape.
23
23
  *
24
- * This module is step 1 of the staged narrowing: it makes the key set VISIBLE
25
- * and warns on anything outside it, without changing a single type. Nothing
26
- * breaks, and an invisible failure becomes an audible one. Steps 2 and 3 —
27
- * promoting the legitimate keys to explicit optional fields, then removing the
28
- * index signature — are what finally let `tsc` catch typos and retired keys.
24
+ * This module was step 1 of the staged narrowing: make the key set VISIBLE and
25
+ * warn on anything outside it, without changing a single type. Step 2 promoted
26
+ * the legitimate keys to explicit optional fields; **step 3 removed the index
27
+ * signature**, so `tsc` now rejects a typo or a retired key outright.
28
+ *
29
+ * ── Why this module SURVIVES step 3 ──────────────────────────────────────────
30
+ * Step 1 wrote that the dev-mode warning "can retire" once the index signature
31
+ * came down. Step 3 measured that and kept it, because the two mechanisms cover
32
+ * DISJOINT populations rather than the same one twice:
33
+ *
34
+ * - `tsc` sees action literals AUTHORED IN CODE. It rejects an unknown key at
35
+ * the construction site, which is the strongest possible signal — but only
36
+ * for actions that exist as TypeScript.
37
+ * - the warning below sees actions that arrive as DATA. objectstack#3903
38
+ * established that stored `sys_metadata` rows are rehydrated UNPARSED, so
39
+ * authored metadata reaches `runner.execute()` as a plain object no
40
+ * compiler ever looked at. That is the population `execute: 'markDone'`
41
+ * actually lives in, and no type can reach it.
42
+ *
43
+ * Retiring the warning would therefore have re-opened the runtime half of the
44
+ * gap while closing the compile-time half — which is why `executeScript`'s
45
+ * rename-prescription branch also survives, for the same rows and the same
46
+ * reason. What DID retire with the index signature is the inverted pin in
47
+ * `actionKeys.pin.test.ts` that asserted the signature was still there; it now
48
+ * asserts the opposite, which was always its stated completion condition.
29
49
  *
30
50
  * The lists are pinned by `__tests__/actionKeys.pin.test.ts`, which re-derives
31
51
  * each one from its actual source. A hand-maintained list that drifts from the
@@ -66,8 +86,22 @@
66
86
  * deprecating them toward a spelling that does not exist would be worse than
67
87
  * leaving them declared.
68
88
  *
69
- * Step 3 remains: remove the index signature, at which point `tsc` catches both
70
- * typos and retired keys and the dev-mode warning below can retire with it.
89
+ * ── What step 3 found the first two steps had missed ─────────────────────────
90
+ * Deleting the index signature is the only thing that can name the keys still
91
+ * hiding behind it, and it named five, in two groups:
92
+ *
93
+ * `to` / `external` / `newTab` / `replace` — the `navigation` alias's own
94
+ * spelling. Step 1 had already ruled these legitimate and listed them below in
95
+ * {@link NAVIGATION_ALIAS_KEYS}, but step 2's scope was the SPEC-owned keys, so
96
+ * they were declared as data and never as fields; `executeNavigation` read them
97
+ * through the index signature. Promoted, and deliberately not `@deprecated` —
98
+ * step 2's acceptance ruled on exactly that point.
99
+ *
100
+ * `description` — objectui vocabulary from `@object-ui/types`' renderer view of
101
+ * an action, forwarded by all four action renderers (`check:action-forward-parity`
102
+ * requires it) and read by the param-collection dialog for its subtitle
103
+ * (objectui#4192). Authorable, forwarded, read — and undeclared. These were the
104
+ * only two `TS2353`s the deletion produced across the whole workspace.
71
105
  */
72
106
  /**
73
107
  * Every property `ActionDef` declares, in declaration order.
@@ -81,6 +115,10 @@ export const ACTION_DEF_KEYS = [
81
115
  'actionType',
82
116
  'name',
83
117
  'label',
118
+ // Promoted by step 3 — objectui vocabulary (`@object-ui/types`' renderer view
119
+ // of an action), forwarded by all four action renderers and read by the
120
+ // param-collection dialog, but never declared on `ActionDef`.
121
+ 'description',
84
122
  'confirmText',
85
123
  'confirm',
86
124
  'condition',
@@ -89,6 +127,14 @@ export const ACTION_DEF_KEYS = [
89
127
  'endpoint',
90
128
  'method',
91
129
  'navigate',
130
+ // The `navigation` alias's own spelling, promoted to real fields by step 3.
131
+ // Listed a second time in `NAVIGATION_ALIAS_KEYS` below, which keeps the
132
+ // separate fact that these four are objectui dialect with a spec-adoption
133
+ // tripwire; this list is only "what the interface declares".
134
+ 'to',
135
+ 'external',
136
+ 'newTab',
137
+ 'replace',
92
138
  'onClick',
93
139
  'reload',
94
140
  'close',
@@ -169,6 +215,15 @@ export const SPEC_ACTION_KEYS = [
169
215
  'bulkEnabled',
170
216
  'component',
171
217
  'confirmText',
218
+ // Added to `ActionSchema` in @objectstack/spec 17.0.0-rc.6. Listed here purely
219
+ // because this array's contract is "every property the spec's `ActionSchema`
220
+ // declares" — restating the spec, not adopting a feature. `ActionDef` does NOT
221
+ // declare `description` and is not changed: whether the runner or any renderer
222
+ // should READ it is a separate question, and `actionKeys.pin.test.ts`'s
223
+ // `ActionDef` half stays green without it. What this entry buys is that an
224
+ // action carrying a `description` is no longer reported as having an unknown
225
+ // key, which is the only thing the inventory is consulted for.
226
+ 'description',
172
227
  'disabled',
173
228
  'errorMessage',
174
229
  'execute',
@@ -217,8 +272,15 @@ export const NAVIGATION_ALIAS_KEYS = ['to', 'external', 'newTab', 'replace'];
217
272
  * Warned about separately from unknown keys, and more loudly: an unknown key is
218
273
  * probably a typo, while a retired key is metadata that used to work. That
219
274
  * distinction is why `executeScript` carries a runtime branch returning the
220
- * rename prescription — a branch that exists solely to compensate for the index
221
- * signature, and that can retire with it in step 3.
275
+ * rename prescription.
276
+ *
277
+ * Step 1 expected that branch to retire with the index signature. It does not,
278
+ * and the reason is the one in this module's header: `tsc` closed the
279
+ * code-authored half of the gap, while `execute: 'markDone'` lives in stored
280
+ * rows that reach the runner UNPARSED (objectstack#3903). The branch reads the
281
+ * key off an untyped view of the action precisely so that `ActionDef` can go on
282
+ * NOT declaring it — declaring it to make the read compile would re-legitimize
283
+ * a tombstone the spec keeps only in order to reject it by name.
222
284
  */
223
285
  export const RETIRED_ACTION_KEYS = {
224
286
  execute: '`execute` was removed in @objectstack/spec 17 (#3855) — rename the key to `target`. ' +
@@ -292,3 +354,80 @@ export function warnOnUnknownActionKeys(action, where = 'ActionRunner') {
292
354
  '(packages/core/src/actions/actionKeys.ts). Warned rather than rejected because `ActionDef` ' +
293
355
  'still carries `[key: string]: any`, so the compiler cannot see this (objectstack#4075).');
294
356
  }
357
+ /**
358
+ * Keys a HOST stashes into an action's `params` for the runner's own plumbing,
359
+ * rather than an author writing them there as a request payload.
360
+ *
361
+ * {@link warnOnDeprecatedObjectParams} has to tell "the author put the payload in
362
+ * `params`" (the shape objectstack#5777 retires) apart from "a host stashed row
363
+ * context there" (a shape the runner itself asks for, and that is not going
364
+ * away). `DeclaredActionsBar` dispatches `params: { _rowRecord: record }`,
365
+ * `RelatedRecordActionsBridge` does the same, and `ObjectGrid` adds
366
+ * `_selectedIds`; the api handler lifts `recordId` out of the field values
367
+ * before it updates. Warning on those would fire on nearly every declared-action
368
+ * click and prescribe `bodyExtra`, which is the wrong home for all of them.
369
+ *
370
+ * The `_` prefix is the existing convention for the stash — `recordId` is its one
371
+ * unprefixed member, so it is listed rather than inferred.
372
+ */
373
+ const HOST_STASHED_PARAM_KEYS = new Set(['recordId']);
374
+ const isAuthoredParamKey = (key) => !key.startsWith('_') && !HOST_STASHED_PARAM_KEYS.has(key);
375
+ /**
376
+ * Does this action carry the DEPRECATED object-form `params` — a static request
377
+ * payload written under the key that means "parameter definitions"?
378
+ *
379
+ * The discriminator is `Array.isArray` and the type guard is `api`, mirroring
380
+ * `@objectstack/spec`'s `inline-action-api-params-to-body-extra` conversion
381
+ * (ADR-0087 D2) key for key. Contract parity with the producer is the point: the
382
+ * conversion rewrites exactly this shape to `bodyExtra` at load, so the runner
383
+ * must recognize exactly the same shape as "the thing that got rewritten".
384
+ *
385
+ * The `api` guard is also what keeps the deprecation out of objectstack#6828's
386
+ * territory — object-form `params` on a `type: 'url'` action is a THIRD meaning
387
+ * (`interpolateTarget`'s `${param.X}` scope, `executeUrl`'s `params.newTab`) and
388
+ * `bodyExtra` is NOT its replacement. Do not widen this guard without that
389
+ * ruling.
390
+ */
391
+ export function hasDeprecatedObjectParams(action) {
392
+ if (!action || typeof action !== 'object')
393
+ return false;
394
+ const { type, actionType, params } = action;
395
+ if ((type ?? actionType) !== 'api')
396
+ return false;
397
+ if (!params || typeof params !== 'object' || Array.isArray(params))
398
+ return false;
399
+ return Object.keys(params).some(isAuthoredParamKey);
400
+ }
401
+ /**
402
+ * Dev-mode only: name the object-form `params` that `@objectstack/spec` 17 already
403
+ * refuses at the authoring door, and prescribe `bodyExtra`.
404
+ *
405
+ * This is the audible half of the compat window the maintainer's 2026-08-06
406
+ * ruling on objectstack#5777 ordered (direction A — a separate key, no same-name
407
+ * union): the runner keeps READING a non-array `params` as the static payload for
408
+ * one version window, says so, and the arm is deleted at 18. The loud half lives
409
+ * at the producer, where it belongs — spec's `params` field refuses the object
410
+ * form outright with a message naming `bodyExtra`, and the ADR-0087 conversion
411
+ * rewrites sources that still carry it. So a warning is the correct severity
412
+ * HERE: by the time metadata reaches this runner the producer has already had its
413
+ * say, and what is left to catch is a host composing an ActionDef in code.
414
+ *
415
+ * Dev-only and warn-once, matching {@link warnOnUnknownActionKeys} — a warning
416
+ * that floods a production console is a warning that gets muted.
417
+ */
418
+ export function warnOnDeprecatedObjectParams(action, where = 'ActionRunner') {
419
+ if (!isDev())
420
+ return;
421
+ if (!hasDeprecatedObjectParams(action))
422
+ return;
423
+ const name = action?.name ?? '(unnamed)';
424
+ const memo = `deprecated-object-params:${String(name)}`;
425
+ if (warned.has(memo))
426
+ return;
427
+ warned.add(memo);
428
+ console.warn(`[${where}] action "${String(name)}" carries a static request payload in \`params\`. ` +
429
+ '`params` is the parameter DEFINITION array (fields collected from the user before the ' +
430
+ 'action runs), never a payload map — put the payload in `bodyExtra` instead ' +
431
+ '(objectstack#5777). Still read as the payload for one version window; the arm is ' +
432
+ 'removed at 18. `bodyExtra` is merged LAST, so it overrides anything left here.');
433
+ }
@@ -18,6 +18,7 @@
18
18
  * @module data-scope
19
19
  * @packageDocumentation
20
20
  */
21
+ import { type ElementDataSourceConfig, type ElementSavedView } from './element-data-source.js';
21
22
  /** ViewData union — matches @objectstack/spec ViewDataSchema */
22
23
  export type ViewDataConfig = {
23
24
  provider: 'object';
@@ -38,17 +39,12 @@ export type ViewDataConfig = {
38
39
  provider: 'value';
39
40
  items: any[];
40
41
  };
41
- /** Element-level data source — matches @objectstack/spec ElementDataSourceSchema */
42
- export interface ElementDataSourceConfig {
43
- object: string;
44
- view?: string;
45
- filter?: Record<string, any>;
46
- sort?: Array<{
47
- field: string;
48
- order: 'asc' | 'desc';
49
- }>;
50
- limit?: number;
51
- }
42
+ /**
43
+ * Re-exported from `./element-data-source.js`, where the binding's type now
44
+ * lives alongside the logic that consumes it. Kept on this module path because
45
+ * that is where it was declared before objectstack#5576.
46
+ */
47
+ export type { ElementDataSourceConfig };
52
48
  /** Fetcher interface — consumers provide the actual fetch implementation */
53
49
  export interface DataFetcher {
54
50
  /** Fetch records for an object */
@@ -82,6 +78,19 @@ export interface DataFetcher {
82
78
  headers?: Record<string, string>;
83
79
  body?: any;
84
80
  }): Promise<any>;
81
+ /**
82
+ * Fetch the object's saved views, so an `ElementDataSource.view` name can be
83
+ * resolved to real columns / filter / sort (objectstack#5576).
84
+ *
85
+ * Optional because most fetchers only read records. A fetcher WITHOUT it
86
+ * cannot honour a named view, and {@link ViewDataProvider.resolveElementDataSource}
87
+ * says so in `error` rather than dropping the key — the whole point of #5576
88
+ * is that a `view` the runtime cannot apply must never look applied.
89
+ *
90
+ * Either shape stored views come in is accepted: a map keyed by view id, or
91
+ * the array of overlay rows an adapter's `listViews()` returns.
92
+ */
93
+ fetchViews?(object: string): Promise<Record<string, ElementSavedView> | ElementSavedView[]>;
85
94
  }
86
95
  /** Resolved data result */
87
96
  export interface ResolvedData {
@@ -138,6 +147,22 @@ export declare class ViewDataProvider {
138
147
  private resolveApi;
139
148
  /** Resolve object-based data (metadata-driven) */
140
149
  private resolveObject;
141
- /** Resolve element-level data source */
150
+ /**
151
+ * Resolve an element-level data source (`PageComponentSchema.dataSource`).
152
+ *
153
+ * `view` is HONOURED here, not dropped. Until objectstack#5576 this method
154
+ * forwarded `filter`/`sort`/`limit` and silently discarded `view`, so a page
155
+ * component that named a saved view got every record the object had — the
156
+ * declared binding was inert while looking applied.
157
+ *
158
+ * Three outcomes, all explicit:
159
+ * - no `view` → unchanged behaviour, the binding's own keys are forwarded;
160
+ * - `view` resolved → its columns become the `fields` projection and its
161
+ * filter/sort/limit the baseline the binding's keys compose with (see
162
+ * {@link composeElementDataSource} for the per-key rule);
163
+ * - `view` unresolvable, or the fetcher cannot list views at all → an
164
+ * `error` naming the view and what the object does have. NOT an empty
165
+ * result, and NOT a silent fall back to the unfiltered object.
166
+ */
142
167
  resolveElementDataSource(config: ElementDataSourceConfig): Promise<ResolvedData>;
143
168
  }
@@ -5,6 +5,20 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ /**
9
+ * @object-ui/core - View Data Provider
10
+ *
11
+ * Resolves @objectstack/spec ViewData discriminated union into
12
+ * a unified data access interface. Supports three data modes:
13
+ *
14
+ * 1. 'object' — Metadata-driven via ObjectStack API (object name → fields/records)
15
+ * 2. 'api' — Custom REST API endpoints (read/write URLs)
16
+ * 3. 'value' — Static data (pre-loaded items array)
17
+ *
18
+ * @module data-scope
19
+ * @packageDocumentation
20
+ */
21
+ import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, resolveSavedView, } from './element-data-source.js';
8
22
  /** Extract records array from various common API response shapes */
9
23
  function extractRecords(data) {
10
24
  if (Array.isArray(data))
@@ -142,12 +156,76 @@ export class ViewDataProvider {
142
156
  };
143
157
  }
144
158
  }
145
- /** Resolve element-level data source */
159
+ /**
160
+ * Resolve an element-level data source (`PageComponentSchema.dataSource`).
161
+ *
162
+ * `view` is HONOURED here, not dropped. Until objectstack#5576 this method
163
+ * forwarded `filter`/`sort`/`limit` and silently discarded `view`, so a page
164
+ * component that named a saved view got every record the object had — the
165
+ * declared binding was inert while looking applied.
166
+ *
167
+ * Three outcomes, all explicit:
168
+ * - no `view` → unchanged behaviour, the binding's own keys are forwarded;
169
+ * - `view` resolved → its columns become the `fields` projection and its
170
+ * filter/sort/limit the baseline the binding's keys compose with (see
171
+ * {@link composeElementDataSource} for the per-key rule);
172
+ * - `view` unresolvable, or the fetcher cannot list views at all → an
173
+ * `error` naming the view and what the object does have. NOT an empty
174
+ * result, and NOT a silent fall back to the unfiltered object.
175
+ */
146
176
  async resolveElementDataSource(config) {
147
- return this.resolve({ provider: 'object', object: config.object }, {
148
- filter: config.filter,
149
- sort: config.sort,
150
- limit: config.limit,
177
+ let view;
178
+ if (config.view) {
179
+ if (!this.fetcher?.fetchViews) {
180
+ return {
181
+ records: [],
182
+ total: 0,
183
+ provider: 'object',
184
+ loading: false,
185
+ error: `dataSource.view "${config.view}" cannot be resolved: this data ` +
186
+ `fetcher does not implement fetchViews().`,
187
+ };
188
+ }
189
+ let views;
190
+ try {
191
+ views = collectSavedViews(await this.fetcher.fetchViews(config.object));
192
+ }
193
+ catch (error) {
194
+ return {
195
+ records: [],
196
+ total: 0,
197
+ provider: 'object',
198
+ loading: false,
199
+ error: error.message,
200
+ };
201
+ }
202
+ view = resolveSavedView(views, config.view, config.object);
203
+ if (!view) {
204
+ return {
205
+ records: [],
206
+ total: 0,
207
+ provider: 'object',
208
+ loading: false,
209
+ error: elementDataSourceViewNotFoundMessage(config.object, config.view, Object.keys(views)),
210
+ };
211
+ }
212
+ }
213
+ const composed = composeElementDataSource(config, view);
214
+ const fields = Array.isArray(composed.columns)
215
+ ? composed.columns.filter((c) => typeof c === 'string' && !!c)
216
+ : undefined;
217
+ return this.resolve({ provider: 'object', object: composed.object }, {
218
+ // `DataFetcher.fetchRecords`'s `filter` is declared
219
+ // `Record<string, any>`, which predates the three filter shapes that
220
+ // legitimately circulate here: a MongoDB-style condition object, an
221
+ // ObjectQL AST node array, and a spec `ViewFilterRule[]`. The last two
222
+ // are arrays, so a saved view's filter does not fit the declared option
223
+ // — the cast is that gap, not a coercion. Widening the declared option
224
+ // is a separate change with its own consumers.
225
+ filter: composed.filter,
226
+ sort: composed.sort,
227
+ limit: composed.limit,
228
+ ...(fields && fields.length > 0 ? { fields } : {}),
151
229
  });
152
230
  }
153
231
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /** Sort entry, as both the spec's `SortItem` and objectui's view configs spell it. */
9
+ export interface ElementDataSourceSort {
10
+ field: string;
11
+ order: 'asc' | 'desc';
12
+ }
13
+ /**
14
+ * Element-level data source — matches `@objectstack/spec` `ElementDataSourceSchema`.
15
+ *
16
+ * `filter` is typed `unknown` rather than the spec's `FilterCondition` because
17
+ * three shapes legitimately reach a renderer here (a MongoDB-style condition
18
+ * object, an ObjectQL AST node array, a spec `ViewFilterRule[]`) and
19
+ * {@link mergeFilterNodes} is the single sink that lowers all three. Narrowing
20
+ * the type here would only move the cast, not remove it.
21
+ */
22
+ export interface ElementDataSourceConfig {
23
+ object: string;
24
+ view?: string;
25
+ filter?: unknown;
26
+ sort?: ElementDataSourceSort[];
27
+ limit?: number;
28
+ }
29
+ /**
30
+ * The subset of a saved view's config that an `ElementDataSource` `view` name
31
+ * contributes. Deliberately loose: stored view metadata is user data and carries
32
+ * both the spec vocabulary and the legacy objectui spellings that
33
+ * `normalizeListViewSchema` folds at the component boundary.
34
+ */
35
+ export type ElementSavedView = Record<string, unknown>;
36
+ /** What {@link composeElementDataSource} produces for a renderer to consume. */
37
+ export interface ComposedElementDataSource {
38
+ /** Object to query. */
39
+ object: string;
40
+ /** Columns the named view shows; absent when no view was named. */
41
+ columns?: unknown;
42
+ /** View filter AND binding filter, combined; `undefined` when neither. */
43
+ filter?: unknown;
44
+ /** Binding sort if given, else the view's. */
45
+ sort?: unknown;
46
+ /** Binding limit if given, else the view's page size. */
47
+ limit?: number;
48
+ /** The view's render kind (grid / kanban / …), when the view declares one. */
49
+ viewType?: string;
50
+ }
51
+ /**
52
+ * Is this value the spec's `dataSource` METADATA, rather than a runtime
53
+ * data-source adapter?
54
+ *
55
+ * This guard exists because the two collide by name and the collision was live:
56
+ * `SchemaRenderer` spreads every non-metadata schema key onto the component as a
57
+ * React prop, so a spec-compliant `dataSource: { object, view }` on a page
58
+ * component landed on `ListView`'s `dataSource` PROP — which is the injected
59
+ * adapter — and shadowed it. The first `dataSource.find(…)` then threw
60
+ * `dataSource.find is not a function`, which the list reported as
61
+ * "Couldn't load records": writing the binding the spec documents BROKE the
62
+ * component (objectstack#5576).
63
+ *
64
+ * The test is positive on the metadata rather than negative on the adapter: a
65
+ * binding is a plain object with a non-empty string `object` and no `find`
66
+ * method. An adapter never has a string `object`; metadata never has `find`.
67
+ */
68
+ export declare function isElementDataSourceConfig(value: unknown): value is ElementDataSourceConfig;
69
+ /**
70
+ * Flatten every place saved views for one object can live into `id → config`.
71
+ *
72
+ * Two sources are real and both are read, in the order app-shell's `ObjectView`
73
+ * reads them: the object definition's embedded `listViews` / `list_views` map
74
+ * (metadata-defined) and the metadata overlay rows an adapter returns from
75
+ * `listViews()` (user-defined). Later sources win, so a saved view shadows a
76
+ * metadata view of the same id — the same precedence `ObjectView` applies when
77
+ * it dedups its tab list.
78
+ *
79
+ * Accepts a record keyed by view id or an array of view configs (overlay rows
80
+ * are an array and carry their identity in `name`, falling back to `id`).
81
+ */
82
+ export declare function collectSavedViews(...sources: unknown[]): Record<string, ElementSavedView>;
83
+ /**
84
+ * Find the saved view a `dataSource.view` name refers to.
85
+ *
86
+ * Name matching is {@link resolveViewId}'s — the same short/qualified tolerance
87
+ * the object page applies to a URL-requested view — so a name that works in one
88
+ * surface works in the other. Returns `undefined` when nothing matches; the
89
+ * caller must REPORT that rather than fall back to the object's default view.
90
+ * Silently widening a named view to "all records" is the failure mode this whole
91
+ * binding exists to remove.
92
+ */
93
+ export declare function resolveSavedView(views: Record<string, ElementSavedView>, requested: string | undefined | null, object: string): ElementSavedView | undefined;
94
+ /**
95
+ * Compose an `ElementDataSource` binding with the saved view it names.
96
+ *
97
+ * Pure — the caller resolves the view (see {@link resolveSavedView}) and owns
98
+ * the "named a view that does not exist" decision. Passing `undefined` /`null`
99
+ * for `view` composes a binding with no view, which is exactly the pre-#5576
100
+ * behaviour for the other four keys.
101
+ *
102
+ * See the module doc for the per-key rule table; `filter` is the only key that
103
+ * combines rather than overrides, because the spec describes the binding's
104
+ * filter as *additional*.
105
+ */
106
+ export declare function composeElementDataSource(config: ElementDataSourceConfig, view?: ElementSavedView | null): ComposedElementDataSource;
107
+ /**
108
+ * The message shown when `dataSource.view` names a view the object does not
109
+ * have. Built here so the render path and `ViewDataProvider` report the same
110
+ * defect the same way, and so the known-view list (the thing that actually gets
111
+ * an author unstuck) is never omitted by one caller.
112
+ */
113
+ export declare function elementDataSourceViewNotFoundMessage(object: string, view: string, knownViews: readonly string[]): string;