@object-ui/core 11.0.0 → 11.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 +17 -0
- package/dist/actions/ActionRunner.d.ts +6 -0
- package/dist/actions/ActionRunner.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/registry/Registry.d.ts +13 -0
- package/dist/registry/Registry.js +30 -0
- package/dist/registry/public-blocks.d.ts +27 -0
- package/dist/registry/public-blocks.js +67 -0
- package/dist/utils/chart-series.d.ts +36 -0
- package/dist/utils/chart-series.js +74 -0
- package/dist/utils/expand-fields.d.ts +41 -11
- package/dist/utils/expand-fields.js +58 -21
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 11.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9e7a986: ADR-0080: AI-authored UI pages. New `@object-ui/sdui-parser` compiles a constrained JSX/HTML+Tailwind source into the SchemaNode tree (parse, never execute) with whitelist sanitization, manifest validation, and `.d.ts` codegen for the JSX type surface. `PageRenderer` renders `kind:'jsx'` pages; `ComponentRegistry` gains `tier` + `getPublicConfigs()` (capability vs contract).
|
|
8
|
+
- 1311749: ADR-0080 M5: curated PUBLIC block contract (capability ≠ contract). Adds `PUBLIC_BLOCKS` — the single, reviewable list of ~36 object-aware + layout/content blocks that form the AI/contract surface (Salesforce-App-Builder-shaped). `getPublicConfigs()` now returns the curated set (plus any `tier:'public'` opt-in), keyed by bare tag and deduped across the registry's dual-key registrations. The full ~244 registered types remain a rendering capability.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- @object-ui/types@11.2.0
|
|
13
|
+
|
|
14
|
+
## 11.1.0
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- @object-ui/types@11.1.0
|
|
19
|
+
|
|
3
20
|
## 7.3.0
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -135,6 +135,12 @@ export interface ActionDef {
|
|
|
135
135
|
execute?: string;
|
|
136
136
|
/** Target URL or identifier (for type: 'url', 'modal', 'flow') */
|
|
137
137
|
target?: string;
|
|
138
|
+
/** For type: 'url' — where to open `target`. `'new-tab'` forces a new
|
|
139
|
+
* browser tab/window, `'self'` forces same-page navigation. When omitted,
|
|
140
|
+
* external URLs open in a new tab and relative URLs navigate in place.
|
|
141
|
+
* This is the public, static "open in new tab" switch (ActionSchema.openIn),
|
|
142
|
+
* distinct from the async-handler `opensInNewTab` pre-open dance. */
|
|
143
|
+
openIn?: 'self' | 'new-tab';
|
|
138
144
|
/** Modal schema to open (for type: 'modal') */
|
|
139
145
|
modal?: any;
|
|
140
146
|
/** Chained actions to execute after this one */
|
|
@@ -468,7 +468,12 @@ export class ActionRunner {
|
|
|
468
468
|
window.location.href = url;
|
|
469
469
|
return { success: true };
|
|
470
470
|
}
|
|
471
|
-
|
|
471
|
+
// `openIn` is the first-class, declarative switch (ActionSchema.openIn);
|
|
472
|
+
// it wins over the legacy `params.newTab` escape hatch and the
|
|
473
|
+
// external-URL heuristic.
|
|
474
|
+
const newTab = action.openIn === 'new-tab' ? true
|
|
475
|
+
: action.openIn === 'self' ? false
|
|
476
|
+
: (action.params?.newTab ?? isExternal);
|
|
472
477
|
if (this.navigationHandler) {
|
|
473
478
|
this.navigationHandler(url, { external: isExternal, newTab });
|
|
474
479
|
return { success: true };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type { SchemaNode, ComponentRendererProps } from './types/index.js';
|
|
9
9
|
export * from './registry/Registry.js';
|
|
10
|
+
export * from './registry/public-blocks.js';
|
|
10
11
|
export * from './registry/PluginSystem.js';
|
|
11
12
|
export * from './registry/PluginScopeImpl.js';
|
|
12
13
|
export * from './registry/WidgetRegistry.js';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
export * from './registry/Registry.js';
|
|
9
|
+
export * from './registry/public-blocks.js';
|
|
9
10
|
export * from './registry/PluginSystem.js';
|
|
10
11
|
export * from './registry/PluginScopeImpl.js';
|
|
11
12
|
export * from './registry/WidgetRegistry.js';
|
|
@@ -25,6 +25,13 @@ export type ComponentMeta = {
|
|
|
25
25
|
label?: string;
|
|
26
26
|
icon?: string;
|
|
27
27
|
category?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Public contract tier (ADR-0080). `'public'` = part of the curated,
|
|
30
|
+
* type-checked, AI-facing block set (gets a strengthened contract, the
|
|
31
|
+
* JSX type surface, the api-surface ratchet, and customer docs). Undefined
|
|
32
|
+
* or `'internal'` = rendering capability only, not part of the contract.
|
|
33
|
+
*/
|
|
34
|
+
tier?: 'public' | 'internal';
|
|
28
35
|
namespace?: string;
|
|
29
36
|
/**
|
|
30
37
|
* When true, prevents the component from being registered with a non-namespaced fallback.
|
|
@@ -182,6 +189,12 @@ export declare class Registry<T = any> {
|
|
|
182
189
|
* @returns Array of all component configurations
|
|
183
190
|
*/
|
|
184
191
|
getAllConfigs(): ComponentConfig<T>[];
|
|
192
|
+
/**
|
|
193
|
+
* Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
|
|
194
|
+
* with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
|
|
195
|
+
* of the full rendering capability returned by {@link getAllConfigs}.
|
|
196
|
+
*/
|
|
197
|
+
getPublicConfigs(): ComponentConfig<T>[];
|
|
185
198
|
/**
|
|
186
199
|
* Get all components in a specific namespace.
|
|
187
200
|
*
|
|
@@ -5,6 +5,7 @@
|
|
|
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
|
+
import { PUBLIC_BLOCKS } from './public-blocks.js';
|
|
8
9
|
export class Registry {
|
|
9
10
|
constructor() {
|
|
10
11
|
Object.defineProperty(this, "components", {
|
|
@@ -270,6 +271,35 @@ export class Registry {
|
|
|
270
271
|
getAllConfigs() {
|
|
271
272
|
return Array.from(this.components.values());
|
|
272
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
|
|
276
|
+
* with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
|
|
277
|
+
* of the full rendering capability returned by {@link getAllConfigs}.
|
|
278
|
+
*/
|
|
279
|
+
getPublicConfigs() {
|
|
280
|
+
// Dedupe by the config's canonical (namespaced) `type` — a component is
|
|
281
|
+
// registered under both a bare and a namespaced key pointing at the same
|
|
282
|
+
// canonical type, and we want one contract entry per component.
|
|
283
|
+
const seenCanonical = new Set();
|
|
284
|
+
const out = [];
|
|
285
|
+
const add = (tag, cfg) => {
|
|
286
|
+
if (!cfg || seenCanonical.has(cfg.type))
|
|
287
|
+
return;
|
|
288
|
+
seenCanonical.add(cfg.type);
|
|
289
|
+
// The contract surface is keyed by the bare/curated tag authors write,
|
|
290
|
+
// not the namespaced canonical stored on the config.
|
|
291
|
+
out.push({ ...cfg, type: tag });
|
|
292
|
+
};
|
|
293
|
+
// Curated contract list first (stable, reviewable order) …
|
|
294
|
+
for (const tag of PUBLIC_BLOCKS)
|
|
295
|
+
add(tag, this.getConfig(tag));
|
|
296
|
+
// … plus any bare registration that opted in explicitly via `tier: 'public'`.
|
|
297
|
+
for (const [key, cfg] of this.components.entries()) {
|
|
298
|
+
if (cfg.tier === 'public' && !key.includes(':'))
|
|
299
|
+
add(key, cfg);
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
273
303
|
/**
|
|
274
304
|
* Get all components in a specific namespace.
|
|
275
305
|
*
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* ADR-0080 — the curated PUBLIC block contract (capability ≠ contract).
|
|
10
|
+
*
|
|
11
|
+
* The subset of registered components that form the platform's *contract* and
|
|
12
|
+
* AI-authoring vocabulary: type-checked, api-surface-ratcheted, documented, and
|
|
13
|
+
* offered to the JSX-source authoring surface. The full ~244 registered types
|
|
14
|
+
* remain a rendering *capability* (`getAllConfigs`); only these are the
|
|
15
|
+
* contract (`getPublicConfigs`).
|
|
16
|
+
*
|
|
17
|
+
* Shaped like Salesforce App Builder standard components — small, object-centric
|
|
18
|
+
* — plus a thin layout/content layer and one escape hatch. A component not yet
|
|
19
|
+
* registered is simply skipped (the list is aspirational-safe). Registrations
|
|
20
|
+
* may also opt in individually via `tier: 'public'`.
|
|
21
|
+
*
|
|
22
|
+
* This is a single, reviewable source of truth for the public surface — prefer
|
|
23
|
+
* editing this list over scattering `tier` flags across registration sites.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PUBLIC_BLOCKS: readonly string[];
|
|
26
|
+
/** Fast membership set built from {@link PUBLIC_BLOCKS}. */
|
|
27
|
+
export declare const PUBLIC_BLOCK_SET: ReadonlySet<string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* ADR-0080 — the curated PUBLIC block contract (capability ≠ contract).
|
|
10
|
+
*
|
|
11
|
+
* The subset of registered components that form the platform's *contract* and
|
|
12
|
+
* AI-authoring vocabulary: type-checked, api-surface-ratcheted, documented, and
|
|
13
|
+
* offered to the JSX-source authoring surface. The full ~244 registered types
|
|
14
|
+
* remain a rendering *capability* (`getAllConfigs`); only these are the
|
|
15
|
+
* contract (`getPublicConfigs`).
|
|
16
|
+
*
|
|
17
|
+
* Shaped like Salesforce App Builder standard components — small, object-centric
|
|
18
|
+
* — plus a thin layout/content layer and one escape hatch. A component not yet
|
|
19
|
+
* registered is simply skipped (the list is aspirational-safe). Registrations
|
|
20
|
+
* may also opt in individually via `tier: 'public'`.
|
|
21
|
+
*
|
|
22
|
+
* This is a single, reviewable source of truth for the public surface — prefer
|
|
23
|
+
* editing this list over scattering `tier` flags across registration sites.
|
|
24
|
+
*/
|
|
25
|
+
export const PUBLIC_BLOCKS = [
|
|
26
|
+
// ── Tier A — object-aware blocks (the contract core) ──────────────────────
|
|
27
|
+
'object-grid',
|
|
28
|
+
'list-view',
|
|
29
|
+
'object-form',
|
|
30
|
+
'embeddable-form',
|
|
31
|
+
'object-master-detail-form',
|
|
32
|
+
'object-kanban',
|
|
33
|
+
'object-calendar',
|
|
34
|
+
'object-gantt',
|
|
35
|
+
'object-timeline',
|
|
36
|
+
'object-map',
|
|
37
|
+
'object-metric',
|
|
38
|
+
'object-chart',
|
|
39
|
+
'dashboard',
|
|
40
|
+
'object-pivot',
|
|
41
|
+
'record:details',
|
|
42
|
+
'record:highlights',
|
|
43
|
+
'record:related_list',
|
|
44
|
+
'record:path',
|
|
45
|
+
'line_items',
|
|
46
|
+
// ── Tier B — layout / content primitives ──────────────────────────────────
|
|
47
|
+
'flex',
|
|
48
|
+
'grid',
|
|
49
|
+
'stack',
|
|
50
|
+
'card',
|
|
51
|
+
'tabs',
|
|
52
|
+
'accordion',
|
|
53
|
+
'container',
|
|
54
|
+
'page:header',
|
|
55
|
+
'text',
|
|
56
|
+
'image',
|
|
57
|
+
'icon',
|
|
58
|
+
'markdown',
|
|
59
|
+
'element:divider',
|
|
60
|
+
'badge',
|
|
61
|
+
'alert',
|
|
62
|
+
'button',
|
|
63
|
+
// ── Tier C — escape hatch (flagged, second-class) ─────────────────────────
|
|
64
|
+
'html',
|
|
65
|
+
];
|
|
66
|
+
/** Fast membership set built from {@link PUBLIC_BLOCKS}. */
|
|
67
|
+
export const PUBLIC_BLOCK_SET = new Set(PUBLIC_BLOCKS);
|
|
@@ -36,6 +36,31 @@ export interface ChartSeriesResult {
|
|
|
36
36
|
series: ChartSeries[];
|
|
37
37
|
}
|
|
38
38
|
export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null): ChartSeriesResult;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve select/enum dimension VALUES to display LABELS in chart rows.
|
|
41
|
+
*
|
|
42
|
+
* Analytics groups by a select field's stored `value` (e.g. `active`), but a
|
|
43
|
+
* chart axis should read the option `label` (e.g. `合作中`). The server SHOULD
|
|
44
|
+
* resolve this (ADR-0021), but when it can't — an AI-built select whose
|
|
45
|
+
* `options` the analytics layer never sees, so its `resolveDimensionLabels`
|
|
46
|
+
* silently no-ops — the rows arrive value-keyed. The axis then shows raw enum
|
|
47
|
+
* values, and (worse) option-keyed colour / category wiring built from the
|
|
48
|
+
* field `label`s no longer lines up with the value-keyed rows, so categories
|
|
49
|
+
* read empty. This is the chart-layer safety net the legacy aggregate path
|
|
50
|
+
* already gets from `resolveGroupByLabels`.
|
|
51
|
+
*
|
|
52
|
+
* Each row is rewritten by replacing `row[dim]` with `labelMaps[dim][value]`
|
|
53
|
+
* when a mapping exists. Measure columns are untouched, so the grouped count
|
|
54
|
+
* stays attached to its (now label-keyed) category — `value` is the matching
|
|
55
|
+
* key, `label` is only the display. Values with no mapping (already a label
|
|
56
|
+
* because the server resolved it, a lookup id, free text) pass through, so this
|
|
57
|
+
* is safe to run unconditionally and is idempotent.
|
|
58
|
+
*
|
|
59
|
+
* Returns a NEW array; a row that needs no change keeps its identity, and the
|
|
60
|
+
* input rows are never mutated, so the server's raw rows survive for
|
|
61
|
+
* index-aligned drill-through (`drillRawRows`).
|
|
62
|
+
*/
|
|
63
|
+
export declare function relabelDimensions(rows: Array<Record<string, unknown>> | null | undefined, labelMaps: Record<string, Record<string, string>> | null | undefined): Array<Record<string, unknown>>;
|
|
39
64
|
/**
|
|
40
65
|
* Inverse of {@link buildChartSeries}: map a clicked chart segment back to the
|
|
41
66
|
* index of its source dataset row, so a chart click can drill through to the
|
|
@@ -64,3 +89,14 @@ export declare function findChartSeriesRow(rows: Array<Record<string, unknown>>
|
|
|
64
89
|
* paint identically across the chart view and dashboard widgets.
|
|
65
90
|
*/
|
|
66
91
|
export declare function buildOptionColorMap(options: unknown): Record<string, string> | null;
|
|
92
|
+
/**
|
|
93
|
+
* Build a `{ value → label }` map from a select/enum field's `options`, for
|
|
94
|
+
* resolving a grouped dimension's stored value to its display label (fed to
|
|
95
|
+
* {@link relabelDimensions}). Mirrors {@link buildOptionColorMap}.
|
|
96
|
+
*
|
|
97
|
+
* Options may be `{ value, label }` objects or bare strings (value == label —
|
|
98
|
+
* nothing to relabel). Only entries whose `label` actually differs from the
|
|
99
|
+
* `value` are kept, so the map is empty (→ `null`) when relabeling would be a
|
|
100
|
+
* no-op and the caller can skip it entirely.
|
|
101
|
+
*/
|
|
102
|
+
export declare function buildDimensionLabelMap(options: unknown): Record<string, string> | null;
|
|
@@ -42,6 +42,53 @@ export function buildChartSeries(rows, dimensions, values, fields) {
|
|
|
42
42
|
series: vals.map((v) => ({ dataKey: v, label: labelOf(v) })),
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve select/enum dimension VALUES to display LABELS in chart rows.
|
|
47
|
+
*
|
|
48
|
+
* Analytics groups by a select field's stored `value` (e.g. `active`), but a
|
|
49
|
+
* chart axis should read the option `label` (e.g. `合作中`). The server SHOULD
|
|
50
|
+
* resolve this (ADR-0021), but when it can't — an AI-built select whose
|
|
51
|
+
* `options` the analytics layer never sees, so its `resolveDimensionLabels`
|
|
52
|
+
* silently no-ops — the rows arrive value-keyed. The axis then shows raw enum
|
|
53
|
+
* values, and (worse) option-keyed colour / category wiring built from the
|
|
54
|
+
* field `label`s no longer lines up with the value-keyed rows, so categories
|
|
55
|
+
* read empty. This is the chart-layer safety net the legacy aggregate path
|
|
56
|
+
* already gets from `resolveGroupByLabels`.
|
|
57
|
+
*
|
|
58
|
+
* Each row is rewritten by replacing `row[dim]` with `labelMaps[dim][value]`
|
|
59
|
+
* when a mapping exists. Measure columns are untouched, so the grouped count
|
|
60
|
+
* stays attached to its (now label-keyed) category — `value` is the matching
|
|
61
|
+
* key, `label` is only the display. Values with no mapping (already a label
|
|
62
|
+
* because the server resolved it, a lookup id, free text) pass through, so this
|
|
63
|
+
* is safe to run unconditionally and is idempotent.
|
|
64
|
+
*
|
|
65
|
+
* Returns a NEW array; a row that needs no change keeps its identity, and the
|
|
66
|
+
* input rows are never mutated, so the server's raw rows survive for
|
|
67
|
+
* index-aligned drill-through (`drillRawRows`).
|
|
68
|
+
*/
|
|
69
|
+
export function relabelDimensions(rows, labelMaps) {
|
|
70
|
+
const safeRows = Array.isArray(rows) ? rows : [];
|
|
71
|
+
if (!labelMaps)
|
|
72
|
+
return safeRows;
|
|
73
|
+
const dims = Object.keys(labelMaps).filter((d) => labelMaps[d] && Object.keys(labelMaps[d]).length > 0);
|
|
74
|
+
if (dims.length === 0)
|
|
75
|
+
return safeRows;
|
|
76
|
+
return safeRows.map((row) => {
|
|
77
|
+
let next = null;
|
|
78
|
+
for (const dim of dims) {
|
|
79
|
+
const raw = row[dim];
|
|
80
|
+
if (raw == null)
|
|
81
|
+
continue;
|
|
82
|
+
const label = labelMaps[dim][String(raw)];
|
|
83
|
+
if (label != null && label !== raw) {
|
|
84
|
+
if (!next)
|
|
85
|
+
next = { ...row };
|
|
86
|
+
next[dim] = label;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return next ?? row;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
45
92
|
/**
|
|
46
93
|
* Inverse of {@link buildChartSeries}: map a clicked chart segment back to the
|
|
47
94
|
* index of its source dataset row, so a chart click can drill through to the
|
|
@@ -100,3 +147,30 @@ export function buildOptionColorMap(options) {
|
|
|
100
147
|
}
|
|
101
148
|
return Object.keys(map).length > 0 ? map : null;
|
|
102
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Build a `{ value → label }` map from a select/enum field's `options`, for
|
|
152
|
+
* resolving a grouped dimension's stored value to its display label (fed to
|
|
153
|
+
* {@link relabelDimensions}). Mirrors {@link buildOptionColorMap}.
|
|
154
|
+
*
|
|
155
|
+
* Options may be `{ value, label }` objects or bare strings (value == label —
|
|
156
|
+
* nothing to relabel). Only entries whose `label` actually differs from the
|
|
157
|
+
* `value` are kept, so the map is empty (→ `null`) when relabeling would be a
|
|
158
|
+
* no-op and the caller can skip it entirely.
|
|
159
|
+
*/
|
|
160
|
+
export function buildDimensionLabelMap(options) {
|
|
161
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
162
|
+
return null;
|
|
163
|
+
const map = {};
|
|
164
|
+
for (const opt of options) {
|
|
165
|
+
if (opt && typeof opt === 'object') {
|
|
166
|
+
const o = opt;
|
|
167
|
+
if (o.value != null && o.label != null) {
|
|
168
|
+
const v = String(o.value);
|
|
169
|
+
const l = String(o.label);
|
|
170
|
+
if (l !== v)
|
|
171
|
+
map[v] = l;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Object.keys(map).length > 0 ? map : null;
|
|
176
|
+
}
|
|
@@ -5,32 +5,62 @@
|
|
|
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
|
+
* Relational ("reference-bearing") field types whose stored value is a foreign
|
|
10
|
+
* key into another object — and which therefore benefit from `$expand` so a
|
|
11
|
+
* list / grid / detail cell can render the related record's display name
|
|
12
|
+
* instead of a bare id placeholder ("—").
|
|
13
|
+
*
|
|
14
|
+
* This mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
|
|
15
|
+
* (`lookup` / `master_detail` / `tree`) and additionally covers `user` — a
|
|
16
|
+
* lookup specialised to `sys_user`. The server resolves `user` through the
|
|
17
|
+
* same expand path as `lookup` / `master_detail` (it carries the same
|
|
18
|
+
* `reference` + id storage), so a `user` column that is NOT requested for
|
|
19
|
+
* expansion comes back as a raw user id and renders as "—". Keeping the set
|
|
20
|
+
* here as the single source of truth is what closes that gap.
|
|
21
|
+
*
|
|
22
|
+
* Note on `tree`: a self-referencing hierarchy field is a reference too, so it
|
|
23
|
+
* belongs in this set; whether the backend materialises the expanded object
|
|
24
|
+
* for it is a server concern — requesting it is harmless and forward-compatible.
|
|
25
|
+
*/
|
|
26
|
+
export declare const EXPANDABLE_FIELD_TYPES: ReadonlySet<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a field definition is a reference-bearing type that can be `$expand`-ed.
|
|
29
|
+
* Only the field's `type` matters here — the `reference` / `reference_to` target
|
|
30
|
+
* is irrelevant to the decision, so this works regardless of which canonical key
|
|
31
|
+
* the schema uses to name the related object.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isExpandableFieldType(fieldDef: unknown): boolean;
|
|
8
34
|
/**
|
|
9
35
|
* Build an array of field names that should be included in `$expand`
|
|
10
36
|
* when fetching data. This scans the given object schema fields
|
|
11
|
-
* (and optional column configuration) for
|
|
12
|
-
*
|
|
13
|
-
*
|
|
37
|
+
* (and optional column configuration) for reference-bearing field types
|
|
38
|
+
* (see {@link EXPANDABLE_FIELD_TYPES}: `lookup` / `master_detail` / `tree` /
|
|
39
|
+
* `user`), so the backend (e.g. objectql) returns expanded objects instead of
|
|
40
|
+
* raw foreign-key IDs.
|
|
14
41
|
*
|
|
15
42
|
* @param schemaFields - Object map of field metadata from `getObjectSchema()`,
|
|
16
|
-
* e.g. `{ account: { type: 'lookup',
|
|
43
|
+
* e.g. `{ account: { type: 'lookup', reference: 'accounts' }, ... }`.
|
|
17
44
|
* @param columns - Optional explicit column list. When provided, only
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
45
|
+
* reference fields that appear in `columns` are expanded — list/grid/kanban
|
|
46
|
+
* views pass their VISIBLE columns here so wide objects don't pay to expand
|
|
47
|
+
* relations no cell will show. Accepts `string[]` or `ListColumn[]` (objects
|
|
48
|
+
* with a `field` property).
|
|
49
|
+
* @returns Array of field names to pass as `$expand` (empty → omit `$expand`).
|
|
21
50
|
*
|
|
22
51
|
* @example
|
|
23
52
|
* ```ts
|
|
24
53
|
* const fields = {
|
|
25
54
|
* name: { type: 'text' },
|
|
26
|
-
* account: { type: 'lookup',
|
|
27
|
-
* parent: { type: 'master_detail',
|
|
55
|
+
* account: { type: 'lookup', reference: 'accounts' },
|
|
56
|
+
* parent: { type: 'master_detail', reference: 'contacts' },
|
|
57
|
+
* assignee: { type: 'user', reference: 'sys_user' },
|
|
28
58
|
* };
|
|
29
59
|
* buildExpandFields(fields);
|
|
30
|
-
* // → ['account', 'parent']
|
|
60
|
+
* // → ['account', 'parent', 'assignee']
|
|
31
61
|
*
|
|
32
62
|
* buildExpandFields(fields, ['name', 'account']);
|
|
33
|
-
* // → ['account']
|
|
63
|
+
* // → ['account'] (only the visible reference columns)
|
|
34
64
|
* ```
|
|
35
65
|
*/
|
|
36
66
|
export declare function buildExpandFields(schemaFields?: Record<string, any> | null, columns?: (string | {
|
|
@@ -5,51 +5,88 @@
|
|
|
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
|
+
* Relational ("reference-bearing") field types whose stored value is a foreign
|
|
10
|
+
* key into another object — and which therefore benefit from `$expand` so a
|
|
11
|
+
* list / grid / detail cell can render the related record's display name
|
|
12
|
+
* instead of a bare id placeholder ("—").
|
|
13
|
+
*
|
|
14
|
+
* This mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
|
|
15
|
+
* (`lookup` / `master_detail` / `tree`) and additionally covers `user` — a
|
|
16
|
+
* lookup specialised to `sys_user`. The server resolves `user` through the
|
|
17
|
+
* same expand path as `lookup` / `master_detail` (it carries the same
|
|
18
|
+
* `reference` + id storage), so a `user` column that is NOT requested for
|
|
19
|
+
* expansion comes back as a raw user id and renders as "—". Keeping the set
|
|
20
|
+
* here as the single source of truth is what closes that gap.
|
|
21
|
+
*
|
|
22
|
+
* Note on `tree`: a self-referencing hierarchy field is a reference too, so it
|
|
23
|
+
* belongs in this set; whether the backend materialises the expanded object
|
|
24
|
+
* for it is a server concern — requesting it is harmless and forward-compatible.
|
|
25
|
+
*/
|
|
26
|
+
export const EXPANDABLE_FIELD_TYPES = new Set([
|
|
27
|
+
'lookup',
|
|
28
|
+
'master_detail',
|
|
29
|
+
'tree',
|
|
30
|
+
'user',
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Whether a field definition is a reference-bearing type that can be `$expand`-ed.
|
|
34
|
+
* Only the field's `type` matters here — the `reference` / `reference_to` target
|
|
35
|
+
* is irrelevant to the decision, so this works regardless of which canonical key
|
|
36
|
+
* the schema uses to name the related object.
|
|
37
|
+
*/
|
|
38
|
+
export function isExpandableFieldType(fieldDef) {
|
|
39
|
+
return (!!fieldDef &&
|
|
40
|
+
typeof fieldDef === 'object' &&
|
|
41
|
+
EXPANDABLE_FIELD_TYPES.has(fieldDef.type));
|
|
42
|
+
}
|
|
8
43
|
/**
|
|
9
44
|
* Build an array of field names that should be included in `$expand`
|
|
10
45
|
* when fetching data. This scans the given object schema fields
|
|
11
|
-
* (and optional column configuration) for
|
|
12
|
-
*
|
|
13
|
-
*
|
|
46
|
+
* (and optional column configuration) for reference-bearing field types
|
|
47
|
+
* (see {@link EXPANDABLE_FIELD_TYPES}: `lookup` / `master_detail` / `tree` /
|
|
48
|
+
* `user`), so the backend (e.g. objectql) returns expanded objects instead of
|
|
49
|
+
* raw foreign-key IDs.
|
|
14
50
|
*
|
|
15
51
|
* @param schemaFields - Object map of field metadata from `getObjectSchema()`,
|
|
16
|
-
* e.g. `{ account: { type: 'lookup',
|
|
52
|
+
* e.g. `{ account: { type: 'lookup', reference: 'accounts' }, ... }`.
|
|
17
53
|
* @param columns - Optional explicit column list. When provided, only
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
54
|
+
* reference fields that appear in `columns` are expanded — list/grid/kanban
|
|
55
|
+
* views pass their VISIBLE columns here so wide objects don't pay to expand
|
|
56
|
+
* relations no cell will show. Accepts `string[]` or `ListColumn[]` (objects
|
|
57
|
+
* with a `field` property).
|
|
58
|
+
* @returns Array of field names to pass as `$expand` (empty → omit `$expand`).
|
|
21
59
|
*
|
|
22
60
|
* @example
|
|
23
61
|
* ```ts
|
|
24
62
|
* const fields = {
|
|
25
63
|
* name: { type: 'text' },
|
|
26
|
-
* account: { type: 'lookup',
|
|
27
|
-
* parent: { type: 'master_detail',
|
|
64
|
+
* account: { type: 'lookup', reference: 'accounts' },
|
|
65
|
+
* parent: { type: 'master_detail', reference: 'contacts' },
|
|
66
|
+
* assignee: { type: 'user', reference: 'sys_user' },
|
|
28
67
|
* };
|
|
29
68
|
* buildExpandFields(fields);
|
|
30
|
-
* // → ['account', 'parent']
|
|
69
|
+
* // → ['account', 'parent', 'assignee']
|
|
31
70
|
*
|
|
32
71
|
* buildExpandFields(fields, ['name', 'account']);
|
|
33
|
-
* // → ['account']
|
|
72
|
+
* // → ['account'] (only the visible reference columns)
|
|
34
73
|
* ```
|
|
35
74
|
*/
|
|
36
75
|
export function buildExpandFields(schemaFields, columns) {
|
|
37
76
|
if (!schemaFields || typeof schemaFields !== 'object') {
|
|
38
77
|
return [];
|
|
39
78
|
}
|
|
40
|
-
// Collect
|
|
41
|
-
const
|
|
79
|
+
// Collect every reference-bearing field name from the schema.
|
|
80
|
+
const referenceFieldNames = [];
|
|
42
81
|
for (const [fieldName, fieldDef] of Object.entries(schemaFields)) {
|
|
43
|
-
if (fieldDef
|
|
44
|
-
|
|
45
|
-
(fieldDef.type === 'lookup' || fieldDef.type === 'master_detail')) {
|
|
46
|
-
lookupFieldNames.push(fieldName);
|
|
82
|
+
if (isExpandableFieldType(fieldDef)) {
|
|
83
|
+
referenceFieldNames.push(fieldName);
|
|
47
84
|
}
|
|
48
85
|
}
|
|
49
|
-
if (
|
|
86
|
+
if (referenceFieldNames.length === 0) {
|
|
50
87
|
return [];
|
|
51
88
|
}
|
|
52
|
-
// When columns are provided, restrict expansion to visible columns only
|
|
89
|
+
// When columns are provided, restrict expansion to visible columns only.
|
|
53
90
|
if (columns && Array.isArray(columns) && columns.length > 0) {
|
|
54
91
|
const columnFieldNames = new Set();
|
|
55
92
|
for (const col of columns) {
|
|
@@ -62,7 +99,7 @@ export function buildExpandFields(schemaFields, columns) {
|
|
|
62
99
|
columnFieldNames.add(name);
|
|
63
100
|
}
|
|
64
101
|
}
|
|
65
|
-
return
|
|
102
|
+
return referenceFieldNames.filter((f) => columnFieldNames.has(f));
|
|
66
103
|
}
|
|
67
|
-
return
|
|
104
|
+
return referenceFieldNames;
|
|
68
105
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@objectstack/spec": "^11.0.0",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "11.
|
|
32
|
+
"@object-ui/types": "11.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|