@object-ui/core 11.0.0 → 11.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 +43 -0
- package/dist/actions/ActionRunner.d.ts +6 -0
- package/dist/actions/ActionRunner.js +6 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -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/runtime/capabilities.d.ts +35 -0
- package/dist/runtime/capabilities.js +72 -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/dist/utils/record-title.d.ts +98 -0
- package/dist/utils/record-title.js +0 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 11.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d23d6eb: Three-tier AI page authoring: `kind:'html'` and a trusted `kind:'react'` tier.
|
|
8
|
+
|
|
9
|
+
- **`@object-ui/react-runtime`** (new) — the trusted runtime-React tier for
|
|
10
|
+
`kind:'react'` pages (vendored react-runner: Sucrase transpile + scope-eval,
|
|
11
|
+
no sandbox). Renders real JSX/TSX (any HTML + JS + hooks/useState/map/onClick)
|
|
12
|
+
in the main React tree with an injected scope (React, the public data blocks,
|
|
13
|
+
page data) and a built-in error boundary.
|
|
14
|
+
- **`@object-ui/core`** — new runtime capability gate (`enableCapability` /
|
|
15
|
+
`disableCapability` / `isCapabilityEnabled`, `CAP_REACT_PAGES`). `react-pages`
|
|
16
|
+
defaults **ON** (the platform trusts reviewed, draft-gated authors); a
|
|
17
|
+
deployment turns it OFF server-side (the runtime injects the disable global
|
|
18
|
+
when `OS_DISABLE_REACT_PAGES` is set). Never controlled from authored metadata.
|
|
19
|
+
- **`@object-ui/components`** — PageRenderer now routes `kind:'react'`
|
|
20
|
+
(capability-gated, lazy-loads the runtime) and renders `kind:'html'` (the
|
|
21
|
+
former `kind:'jsx'`, still accepted as a deprecated alias). The `html` tier
|
|
22
|
+
now resolves the full safe native HTML tag set (h1–h6, p, a, ul/ol/li, img,
|
|
23
|
+
blockquote, pre, strong/em, …) so authored HTML lives up to its name.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- @object-ui/types@11.3.0
|
|
28
|
+
|
|
29
|
+
## 11.2.0
|
|
30
|
+
|
|
31
|
+
### Minor Changes
|
|
32
|
+
|
|
33
|
+
- 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).
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- @object-ui/types@11.2.0
|
|
39
|
+
|
|
40
|
+
## 11.1.0
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- @object-ui/types@11.1.0
|
|
45
|
+
|
|
3
46
|
## 7.3.0
|
|
4
47
|
|
|
5
48
|
### 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';
|
|
@@ -28,6 +29,7 @@ export * from './utils/merge-views-into-objects.js';
|
|
|
28
29
|
export * from './utils/freeze-schema.js';
|
|
29
30
|
export * from './protocols/index.js';
|
|
30
31
|
export * from './styling/scoped-styles.js';
|
|
32
|
+
export * from './runtime/capabilities.js';
|
|
31
33
|
/**
|
|
32
34
|
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
33
35
|
*
|
|
@@ -40,3 +42,4 @@ export * from './utils/date-macros.js';
|
|
|
40
42
|
export * from './utils/compare-to.js';
|
|
41
43
|
export * from './utils/chart-series.js';
|
|
42
44
|
export * from './utils/dataset-format.js';
|
|
45
|
+
export * from './utils/record-title.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';
|
|
@@ -27,6 +28,7 @@ export * from './utils/merge-views-into-objects.js';
|
|
|
27
28
|
export * from './utils/freeze-schema.js';
|
|
28
29
|
export * from './protocols/index.js';
|
|
29
30
|
export * from './styling/scoped-styles.js';
|
|
31
|
+
export * from './runtime/capabilities.js';
|
|
30
32
|
/**
|
|
31
33
|
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
32
34
|
*
|
|
@@ -39,3 +41,4 @@ export * from './utils/date-macros.js';
|
|
|
39
41
|
export * from './utils/compare-to.js';
|
|
40
42
|
export * from './utils/chart-series.js';
|
|
41
43
|
export * from './utils/dataset-format.js';
|
|
44
|
+
export * from './utils/record-title.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);
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
* Runtime capability gates — switches a *host* controls, never authored
|
|
9
|
+
* metadata. Each capability has a default; a host can flip it explicitly
|
|
10
|
+
* (`enableCapability`/`disableCapability`) or seed it from globals before any
|
|
11
|
+
* bundle code runs.
|
|
12
|
+
*
|
|
13
|
+
* {@link CAP_REACT_PAGES} (`kind:'react'` — execute author JavaScript in the
|
|
14
|
+
* main React tree) defaults **ON**: ObjectStack pages are authored by trusted
|
|
15
|
+
* authors and pass human review (draft-gating, ADR-0033), so the platform
|
|
16
|
+
* trusts them by default. A deployment that does NOT trust its page authors
|
|
17
|
+
* turns it off server-side — the ObjectStack runtime injects the disable global
|
|
18
|
+
* when `OS_PAGE_REACT=off` (see framework cli/utils/console.ts).
|
|
19
|
+
*
|
|
20
|
+
* Globals a host/server can set in the page before the bundle runs:
|
|
21
|
+
* - `globalThis.__OBJECTUI_CAPABILITIES__` — string[] to force ON
|
|
22
|
+
* - `globalThis.__OBJECTUI_CAPABILITIES_DISABLED__` — string[] to force OFF
|
|
23
|
+
*/
|
|
24
|
+
/** Turn a capability on. Call from the host process only (not from metadata). */
|
|
25
|
+
export declare function enableCapability(name: string): void;
|
|
26
|
+
/** Turn a capability off. */
|
|
27
|
+
export declare function disableCapability(name: string): void;
|
|
28
|
+
/** Whether a capability is enabled. Host/global override wins over the default. */
|
|
29
|
+
export declare function isCapabilityEnabled(name: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* `kind:'react'` — execute trusted author JavaScript (full React: hooks, event
|
|
32
|
+
* handlers, arbitrary JS) directly in the main React tree. Default ON; a
|
|
33
|
+
* deployment disables it server-side with `OS_PAGE_REACT=off`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CAP_REACT_PAGES = "react-pages";
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
* Runtime capability gates — switches a *host* controls, never authored
|
|
9
|
+
* metadata. Each capability has a default; a host can flip it explicitly
|
|
10
|
+
* (`enableCapability`/`disableCapability`) or seed it from globals before any
|
|
11
|
+
* bundle code runs.
|
|
12
|
+
*
|
|
13
|
+
* {@link CAP_REACT_PAGES} (`kind:'react'` — execute author JavaScript in the
|
|
14
|
+
* main React tree) defaults **ON**: ObjectStack pages are authored by trusted
|
|
15
|
+
* authors and pass human review (draft-gating, ADR-0033), so the platform
|
|
16
|
+
* trusts them by default. A deployment that does NOT trust its page authors
|
|
17
|
+
* turns it off server-side — the ObjectStack runtime injects the disable global
|
|
18
|
+
* when `OS_PAGE_REACT=off` (see framework cli/utils/console.ts).
|
|
19
|
+
*
|
|
20
|
+
* Globals a host/server can set in the page before the bundle runs:
|
|
21
|
+
* - `globalThis.__OBJECTUI_CAPABILITIES__` — string[] to force ON
|
|
22
|
+
* - `globalThis.__OBJECTUI_CAPABILITIES_DISABLED__` — string[] to force OFF
|
|
23
|
+
*/
|
|
24
|
+
/** Default state per capability when neither the host nor a global overrides it. */
|
|
25
|
+
const DEFAULTS = {
|
|
26
|
+
'react-pages': true,
|
|
27
|
+
};
|
|
28
|
+
// Explicit host overrides (enable/disable) — win over defaults and globals.
|
|
29
|
+
const overrides = new Map();
|
|
30
|
+
let _seeded = false;
|
|
31
|
+
function seedFromGlobals() {
|
|
32
|
+
if (_seeded)
|
|
33
|
+
return;
|
|
34
|
+
_seeded = true;
|
|
35
|
+
const g = globalThis;
|
|
36
|
+
if (Array.isArray(g.__OBJECTUI_CAPABILITIES__)) {
|
|
37
|
+
for (const c of g.__OBJECTUI_CAPABILITIES__)
|
|
38
|
+
if (typeof c === 'string' && !overrides.has(c))
|
|
39
|
+
overrides.set(c, true);
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(g.__OBJECTUI_CAPABILITIES_DISABLED__)) {
|
|
42
|
+
// Disable wins: a server that turned something off must not be overridden
|
|
43
|
+
// by a stale enable-list left in the same page.
|
|
44
|
+
for (const c of g.__OBJECTUI_CAPABILITIES_DISABLED__)
|
|
45
|
+
if (typeof c === 'string')
|
|
46
|
+
overrides.set(c, false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Turn a capability on. Call from the host process only (not from metadata). */
|
|
50
|
+
export function enableCapability(name) {
|
|
51
|
+
seedFromGlobals();
|
|
52
|
+
overrides.set(name, true);
|
|
53
|
+
}
|
|
54
|
+
/** Turn a capability off. */
|
|
55
|
+
export function disableCapability(name) {
|
|
56
|
+
seedFromGlobals();
|
|
57
|
+
overrides.set(name, false);
|
|
58
|
+
}
|
|
59
|
+
/** Whether a capability is enabled. Host/global override wins over the default. */
|
|
60
|
+
export function isCapabilityEnabled(name) {
|
|
61
|
+
seedFromGlobals();
|
|
62
|
+
const o = overrides.get(name);
|
|
63
|
+
if (o !== undefined)
|
|
64
|
+
return o;
|
|
65
|
+
return DEFAULTS[name] ?? false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `kind:'react'` — execute trusted author JavaScript (full React: hooks, event
|
|
69
|
+
* handlers, arbitrary JS) directly in the main React tree. Default ON; a
|
|
70
|
+
* deployment disables it server-side with `OS_PAGE_REACT=off`.
|
|
71
|
+
*/
|
|
72
|
+
export const CAP_REACT_PAGES = 'react-pages';
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
* Render a record title from a `titleFormat` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Accepts either a legacy string template or an Expression envelope
|
|
12
|
+
* (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
|
|
13
|
+
* normalized templates. Both single-brace (`{field}`) and double-brace
|
|
14
|
+
* (`{{field}}`, the handlebars-style some authoring paths emit) placeholders are
|
|
15
|
+
* supported. Dotted paths (`{account.name}`) walk `$expanded` lookup objects; a
|
|
16
|
+
* placeholder that resolves to a whole object is reduced to its display name.
|
|
17
|
+
*
|
|
18
|
+
* Empty placeholders (missing / null / empty fields) are stripped along with
|
|
19
|
+
* any orphan separator they leave behind, so `"{full_name} - {company}"`
|
|
20
|
+
* evaluated against `{ company: "Acme" }` yields `"Acme"`, not `" - Acme"`.
|
|
21
|
+
* Returns an empty string when no placeholder resolved.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatTitleTemplate(titleFormat: string | {
|
|
24
|
+
source?: string;
|
|
25
|
+
} | undefined | null, record: any): string;
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether a field definition can serve as a record title. Excludes the
|
|
28
|
+
* {@link NON_TITLE_TYPES} (dates, numbers, pickers, lookups, binary, …); a
|
|
29
|
+
* `formula` is eligible only when it returns text. A field with no declared
|
|
30
|
+
* type is treated as eligible (text-by-default), so loosely-typed metadata
|
|
31
|
+
* still produces a name rather than falling through to `Record #…`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isTitleEligibleField(def: any): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Type-aware derivation (precedence step 4): pick the best title-eligible field
|
|
36
|
+
* name from `objectDef.fields`, ranked:
|
|
37
|
+
*
|
|
38
|
+
* 1. name-ish exact — `name`,`title`,`subject`,`label`,`full_name`,
|
|
39
|
+
* `display_name` (case-insensitive)
|
|
40
|
+
* 2. name-ish affix — `*_name`, `*_title`, `name_*` (case-insensitive)
|
|
41
|
+
* 3. declaration order — first remaining title-eligible field
|
|
42
|
+
*
|
|
43
|
+
* Returns the field name, or undefined when `fields` is absent/empty or holds
|
|
44
|
+
* no title-eligible field.
|
|
45
|
+
*
|
|
46
|
+
* Memoized per `objectDef` identity (see {@link deriveTitleFieldCache}): the
|
|
47
|
+
* field scan runs once per object definition, not once per record. The result is
|
|
48
|
+
* byte-identical to the uncached computation — only the scan is cached, and only
|
|
49
|
+
* when `objectDef` is a non-null object (the WeakMap key requirement).
|
|
50
|
+
*/
|
|
51
|
+
export declare function deriveTitleField(objectDef: any): string | undefined;
|
|
52
|
+
export interface RecordDisplayNameOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Optional explicit title field (e.g. a view's `titleField` / gallery
|
|
55
|
+
* `titleField` / kanban `cardTitle`). When set and it yields a non-empty
|
|
56
|
+
* value on the record, it wins over everything — preserving each view's
|
|
57
|
+
* author-chosen field. Pass nothing to use the object-level precedence only.
|
|
58
|
+
*/
|
|
59
|
+
titleField?: string;
|
|
60
|
+
/** Floor when the record has no id at all. Defaults to `'Untitled'`. */
|
|
61
|
+
fallback?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether to probe standard name-ish keys (`name`/`title`/…) directly on the
|
|
64
|
+
* record when `objectDef.fields` is absent (precedence step 3b). Defaults to
|
|
65
|
+
* `true`. Callers that have a better generic fallback to interleave (e.g. the
|
|
66
|
+
* detail header prefers `schema.title`, the object label, over a guessed
|
|
67
|
+
* record key) pass `false` so this step is skipped and the resolver returns
|
|
68
|
+
* its `Record #<id>` floor instead — letting the caller decide.
|
|
69
|
+
*/
|
|
70
|
+
deriveFromRecordKeys?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* THE unified record display-name resolver (ADR-0079).
|
|
74
|
+
*
|
|
75
|
+
* Precedence (ADR-0079 Phase 2 makes the object's declared field canonical and
|
|
76
|
+
* de-prioritizes the legacy `titleFormat` template):
|
|
77
|
+
* 0. `options.titleField`, then `objectDef.titleField` (when provided &
|
|
78
|
+
* non-empty on the record) — lets a view / object keep its author-chosen
|
|
79
|
+
* title field.
|
|
80
|
+
* 1. `objectDef.nameField` — the NEW canonical record-title pointer
|
|
81
|
+
* (ADR-0079). A field name the server can return and query.
|
|
82
|
+
* 2. `objectDef.displayNameField` / `objectDef.NAME_FIELD_KEY` — the
|
|
83
|
+
* deprecated alias of `nameField`, kept for back-compat.
|
|
84
|
+
* 3. `objectDef.titleFormat` (rendered template) — LEGACY, render-only, kept
|
|
85
|
+
* for back-compat only; an explicitly-declared field (1/2) now wins over
|
|
86
|
+
* it. See the deprecation note on the branch below.
|
|
87
|
+
* 4. type-aware derivation from `objectDef.fields` ({@link deriveTitleField}).
|
|
88
|
+
* 4b. standard name-ish keys probed directly on the record
|
|
89
|
+
* ({@link NAME_ISH_RECORD_KEYS}) — the safety net for when `fields` is
|
|
90
|
+
* absent so derivation can't run.
|
|
91
|
+
* 5. `Record #<id>` (using `record.id ?? record._id`), else `options.fallback`
|
|
92
|
+
* (default `'Untitled'`) for a truly id-less record.
|
|
93
|
+
*
|
|
94
|
+
* @param objectDef Object definition (may be `any`/partial — every field is
|
|
95
|
+
* read defensively).
|
|
96
|
+
* @param record The record data.
|
|
97
|
+
*/
|
|
98
|
+
export declare function getRecordDisplayName(objectDef: any, record: any, options?: RecordDisplayNameOptions): string;
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/formula": "^11.
|
|
29
|
-
"@objectstack/spec": "^11.
|
|
28
|
+
"@objectstack/formula": "^11.2.0",
|
|
29
|
+
"@objectstack/spec": "^11.2.0",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "11.
|
|
32
|
+
"@object-ui/types": "11.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|