@object-ui/react 17.2.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 (36) hide show
  1. package/CHANGELOG.md +956 -0
  2. package/README.md +39 -13
  3. package/dist/SchemaRenderer.d.ts +78 -5
  4. package/dist/SchemaRenderer.js +139 -13
  5. package/dist/context/AppShellContext.d.ts +21 -2
  6. package/dist/context/RecordContext.d.ts +0 -4
  7. package/dist/context/RecordContext.js +0 -2
  8. package/dist/context/RelatedRecordActionsContext.d.ts +29 -0
  9. package/dist/context/RelatedRecordActionsContext.js +6 -0
  10. package/dist/element-data-source/ElementDataSourceGate.d.ts +199 -0
  11. package/dist/element-data-source/ElementDataSourceGate.js +195 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +10 -0
  14. package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
  15. package/dist/hooks/useActionTextLocalizer.js +91 -0
  16. package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
  17. package/dist/hooks/useDatasetDimensionLabels.js +154 -0
  18. package/dist/hooks/useElementDataSource.d.ts +68 -0
  19. package/dist/hooks/useElementDataSource.js +138 -0
  20. package/dist/hooks/useExpression.d.ts +94 -17
  21. package/dist/hooks/useExpression.js +96 -34
  22. package/dist/hooks/useNavigationOverlay.d.ts +58 -10
  23. package/dist/hooks/useOffline.d.ts +62 -36
  24. package/dist/hooks/useOffline.js +2 -2
  25. package/dist/hooks/usePageAssignment.js +4 -4
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +5 -1
  28. package/dist/schema-input.d.ts +31 -0
  29. package/dist/schema-input.js +31 -0
  30. package/dist/spec-bridge/SpecBridge.d.ts +11 -5
  31. package/dist/spec-bridge/SpecBridge.js +7 -1
  32. package/dist/spec-bridge/bridges/list-view.js +106 -14
  33. package/dist/spec-bridge/types.d.ts +24 -3
  34. package/dist/utils/i18n.d.ts +43 -8
  35. package/dist/utils/i18n.js +42 -4
  36. package/package.json +10 -10
@@ -40,40 +40,100 @@ export function usePredicateScope() {
40
40
  }
41
41
  /**
42
42
  * Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
43
- * `hidden`) into the `${expr}` template form expected by `useCondition`.
44
- *
45
- * Accepts:
46
- * - `boolean` → returned as-is (predicate hooks short-circuit on booleans).
47
- * - `string` → wrapped as `${string}` (legacy DX shorthand).
48
- * - `Expression` envelope `{ dialect, source }` (new format from
49
- * `@objectstack/spec`'s normalized predicate inputs) → unwrapped, then
50
- * wrapped as `${source}`. Both `cel` and `template` dialects already use
51
- * compatible variable syntax (`record.x`, etc.).
52
- * - `null` / `undefined` / empty → `undefined` (default visible/enabled).
53
- *
54
- * This is the canonical helper to use in renderers so we never end up with
55
- * `${[object Object]}` after JS template-literal interpolation.
43
+ * `hidden`) into the form `useCondition` expects — the canonical helper to use
44
+ * in renderers so we never end up with `${[object Object]}` after JS
45
+ * template-literal interpolation.
46
+ *
47
+ * **Re-exported from `@object-ui/core`, not reimplemented here.** There is
48
+ * exactly ONE implementation — `packages/core/src/evaluator/predicateInput.ts`
49
+ * — shared by the engine path (`ActionEngine.getActionsForLocation`) and the
50
+ * renderer path (`action-button` / `action-menu` / `action-bar` / …). See that
51
+ * file for the accepted input shapes and for why a `cel` envelope must survive
52
+ * normalization (#2661 / #3314).
53
+ *
54
+ * ## Why a re-export and not a twin (#3367)
55
+ *
56
+ * This module used to carry an independent renderer-side implementation with
57
+ * item-for-item identical semantics, held in step with the canonical one by a
58
+ * 14-shape normalization parity table. That table was a *guardrail against
59
+ * drift*, not a single source of truth — and #3314 is the record of what two
60
+ * normalizations do when left alone: they drift, and the same `visible:`
61
+ * predicate reaches different verdicts depending on which path surfaced the
62
+ * action. A re-export cannot drift, so the parity suite now pins identity
63
+ * (`react`'s export IS `core`'s function) instead of enumerating shapes, and
64
+ * keeps pinning the two PATHS verdict-for-verdict.
65
+ *
66
+ * Routing hook code through the `@object-ui/core` barrel is not a new coupling:
67
+ * this module already imports `ExpressionEvaluator` / `evalRowPredicate` from
68
+ * it, and `@object-ui/core` is a declared dependency of `@object-ui/react`
69
+ * (the reverse direction is the forbidden one — core declares "Zero React
70
+ * dependencies").
71
+ */
72
+ export { toPredicateInput } from '@object-ui/core';
73
+ /**
74
+ * Build the predicate context for a **row record** — the bag to hand
75
+ * {@link useCondition} when the thing being gated is scoped to one record.
76
+ *
77
+ * The row is bound the THREE ways the platform's row surfaces bind it:
78
+ * `record.status` (spec/canonical), bare `status` (row-action shorthand), and
79
+ * `data.status` (legacy). This is not three dialects; it is one rule, and it is
80
+ * `evalRowPredicate`'s rule (`core/evaluator/listConditional.ts` — the record
81
+ * header, list rows, the row kebab and conditional formatting all evaluate
82
+ * through it), restated here for the `useCondition` tier so both tiers answer
83
+ * an author's `visible:` the same way.
84
+ *
85
+ * ## Why a helper and not "just spread the row" (objectui#4075)
86
+ *
87
+ * `useCondition` evaluates on `new ExpressionEvaluator({ ...scope, ...context })`,
88
+ * so a caller that passes the row spread flat resolves the shorthand spelling
89
+ * and NOTHING else. The canonical spelling is the `record.` root — it is what
90
+ * `ExpressionEvaluator`'s CEL path binds (`bag.record` as the record
91
+ * namespace), what `evalRowPredicate` binds, and what the server enforces
92
+ * with. Under a root-only bag `record.viewer.can_act` does not read as `false`:
93
+ * the evaluator throws `record is not defined`, and a fail-closed `visible`
94
+ * turns that throw into "hidden". A correctly-authored predicate then deletes
95
+ * its own button, indistinguishably from the gate having said no.
96
+ *
97
+ * That was live, not theoretical: every declared action on framework's
98
+ * `sys_approval_request` gates on `record.viewer.*` (framework#3310 / #3424),
99
+ * so the whole server-declared approval decision set was invisible wherever
100
+ * `DeclaredActionsBar` rendered until objectui#4077 bound the row this way —
101
+ * and the four generic action renderers carried the same root-only binding
102
+ * until objectui#4075.
103
+ *
104
+ * `record` and `data` are written AFTER the spread deliberately: a row that
105
+ * happens to carry a field literally named `record` or `data` must not shadow
106
+ * the root every predicate is written against. Same precedence as
107
+ * `evalRowPredicate`.
108
+ *
109
+ * This is the BINDING rule only. It deliberately does not touch the evaluation
110
+ * entry — `useCondition` / `toPredicateInput` / `hasDeclaredVisibilityGate`
111
+ * keep their pinned semantics (objectui#3492 / #3842 / #3850 / #3871), and each
112
+ * caller keeps its own error policy (fail-closed `visible` vs the fail-soft
113
+ * legs). Binding the row is a different question from what to do when the
114
+ * predicate faults.
115
+ *
116
+ * ## No row → bind NOTHING, do not bind an empty one
117
+ *
118
+ * `null` / `undefined` / a non-object returns an EMPTY bag, not
119
+ * `{ record: {}, data: {} }`. The difference is load-bearing: `useCondition`
120
+ * merges this context OVER the ambient predicate scope, so binding an empty
121
+ * record would blank out a `record` that a host had put in the scope itself —
122
+ * which is exactly how `action:group`'s dropdown leaf is driven in
123
+ * `action-group-dropdown-visible.test.tsx`, and it is a legitimate way for a
124
+ * host to supply the row. "This surface has no row of its own" must stay
125
+ * distinct from "this surface's row is empty"; only the latter is entitled to
126
+ * shadow the scope.
127
+ *
128
+ * @param record The row, or nothing.
56
129
  */
57
- export function toPredicateInput(value) {
58
- if (value === null || value === undefined || value === '')
59
- return undefined;
60
- if (typeof value === 'boolean')
61
- return value;
62
- if (typeof value === 'string')
63
- return `\${${value}}`;
64
- if (typeof value === 'object' && typeof value.source === 'string') {
65
- const src = value.source;
66
- if (!src)
67
- return undefined;
68
- // #2661 — preserve a CEL-dialect envelope so `useCondition` routes it to the
69
- // canonical `@objectstack/formula` engine (identical verdict to the server),
70
- // instead of collapsing it to a `${source}` string on the legacy JS path.
71
- // Every other dialect (template / unset) keeps the legacy `${…}` behavior.
72
- if (value.dialect === 'cel')
73
- return { dialect: 'cel', source: src };
74
- return `\${${src}}`;
75
- }
76
- return undefined;
130
+ export function usePredicateRecordContext(record) {
131
+ return useMemo(() => {
132
+ if (record == null || typeof record !== 'object' || Array.isArray(record))
133
+ return {};
134
+ const row = record;
135
+ return { ...row, record: row, data: row };
136
+ }, [record]);
77
137
  }
78
138
  /**
79
139
  * Hook for evaluating expressions with dynamic context
@@ -160,6 +220,7 @@ export function useCondition(condition, context = {}, options) {
160
220
  export function useRowPredicate(pred, row, options) {
161
221
  const scope = usePredicateScope();
162
222
  const fallback = options?.fallback ?? true;
223
+ const fields = options?.fields;
163
224
  return useMemo(() => {
164
225
  // A boolean predicate short-circuits (no expression to evaluate).
165
226
  if (typeof pred === 'boolean')
@@ -171,6 +232,7 @@ export function useRowPredicate(pred, row, options) {
171
232
  scope,
172
233
  warnOnError: options?.warnOnError,
173
234
  label: options?.label,
235
+ fields: fields,
174
236
  });
175
- }, [pred, row, scope, fallback, options?.warnOnError, options?.label]);
237
+ }, [pred, row, scope, fallback, options?.warnOnError, options?.label, fields]);
176
238
  }
@@ -5,10 +5,11 @@
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 type { NavigationConfigSchema } from '@objectstack/spec/ui';
8
+ import type { NavigationConfigSchema, NavigationMode as SpecNavigationMode } from '@objectstack/spec/ui';
9
9
  import type { SpecAuthoredInput } from '../spec-input';
10
10
  /**
11
- * The spec's `NavigationConfigSchema`, authoring side, with `mode` required.
11
+ * The spec's `NavigationConfigSchema`, authoring side — by reference, with no
12
+ * divergence of its own.
12
13
  *
13
14
  * This was a hand copy carrying the note "inline … to avoid importing from
14
15
  * @object-ui/types (which may not be a direct dependency of @object-ui/react)".
@@ -18,10 +19,35 @@ import type { SpecAuthoredInput } from '../spec-input';
18
19
  * `@object-ui/app-shell`, where the dependency had likewise been there all
19
20
  * along. Check `package.json` before believing such a note (objectstack#4115).
20
21
  *
21
- * The ONE divergence: the spec defaults `mode`, so its authoring side makes it
22
- * optional; this hook dispatches on `mode` and its callers always supply one,
23
- * so it is required here. Every other key is the spec's, by reference. Pinned
24
- * by `__tests__/offline-nav-performance-spec-parity.test.ts`.
22
+ * `mode` is OPTIONAL, because the spec says so and this hook agrees.
23
+ * `packages/spec/src/ui/view.zod.ts` declares
24
+ * `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the
25
+ * AUTHORING side as `| undefined` — so `navigation: { view: 'summary_view' }`
26
+ * is legal authored metadata that lets the mode default.
27
+ *
28
+ * Until objectui#4550 this alias `Omit`ted `mode` and re-added it as
29
+ * `NonNullable<…>`, on the stated reasoning that "this hook dispatches on
30
+ * `mode` and its callers always supply one". Both halves were false.
31
+ * `useNavigationOverlay` does not require `mode` — it DEFAULTS it
32
+ * (`navigation?.mode ?? 'page'`, ~140 lines below), and `'page'` is meaningful
33
+ * behaviour rather than a placeholder. And callers did not always supply one:
34
+ * `ListView` carried `schema.navigation as NavigationConfig | undefined` purely
35
+ * to get a spec-shaped value past this declaration — a value the hook had
36
+ * always been willing to take.
37
+ *
38
+ * The rule that makes this a producer-side fix rather than a caller-side one:
39
+ * a type in front of an implementation must not be stricter than the
40
+ * implementation. When it is, every caller pays in assertions, and an assertion
41
+ * is exactly the renderer-side workaround AGENTS.md #0.1 sends back to the
42
+ * producer. Here the producer was this line.
43
+ *
44
+ * `@object-ui/types` re-exports the spec's own `NavigationConfig` unchanged, so
45
+ * that published name and this one now agree — they did not before, which is
46
+ * how one monorepo shipped two `NavigationConfig`s that disagreed about whether
47
+ * `mode` could be omitted. Pinned by
48
+ * `__tests__/offline-nav-performance-spec-parity.test.ts` (the type) and
49
+ * `__tests__/useNavigationOverlay.modeDefault.test.tsx` (the default behaviour
50
+ * the relaxation rests on).
25
51
  *
26
52
  * Two per-key notes the hand copy carried, kept here because a derived alias
27
53
  * has no members to hang them on:
@@ -30,9 +56,7 @@ import type { SpecAuthoredInput } from '../spec-input';
30
56
  * - `width` is DEPRECATED by #2578 in favour of `size`. It still wins when
31
57
  * present, because app-shell pre-resolves `size` into it.
32
58
  */
33
- export type NavigationConfig = Omit<SpecAuthoredInput<typeof NavigationConfigSchema>, 'mode'> & {
34
- mode: NonNullable<SpecAuthoredInput<typeof NavigationConfigSchema>['mode']>;
35
- };
59
+ export type NavigationConfig = SpecAuthoredInput<typeof NavigationConfigSchema>;
36
60
  /**
37
61
  * Resolve the overlay width from a NavigationConfig: an explicit `width` wins
38
62
  * (app-shell pre-resolves `size` into it); otherwise a declared bucket maps
@@ -41,7 +65,31 @@ export type NavigationConfig = Omit<SpecAuthoredInput<typeof NavigationConfigSch
41
65
  * count, which only schema-aware hosts have. Exported for the parity test.
42
66
  */
43
67
  export declare function resolveOverlayWidth(navigation: NavigationConfig | undefined): string | number | undefined;
44
- export type NavigationMode = NavigationConfig['mode'];
68
+ /**
69
+ * The overlay modes — the spec's own union, DERIVED since objectui#4167.
70
+ *
71
+ * rc.6 publishes `NavigationMode` (`z.input<typeof NavigationModeSchema>`), and
72
+ * this alias resolved to exactly it already: `NavigationConfig['mode']` was then
73
+ * `NonNullable<…['mode']>`, and stripping the `undefined` that the schema's
74
+ * `.default()` puts on the authoring side leaves the seven-member enum itself.
75
+ * So the spec reference was one hop away rather than absent — the alias just
76
+ * reached it through a member access, which reads as a hand-written union to
77
+ * `check:spec-symbols` and, more to the point, to a person.
78
+ *
79
+ * Bound to the spec directly instead: the seven members now arrive from the
80
+ * schema that validates them. `__tests__/offline-nav-performance-spec-parity.test.ts`
81
+ * pins that this stays the same type as `NonNullable<NavigationConfig['mode']>`,
82
+ * so the two spellings cannot silently come apart if the spec ever stops
83
+ * defaulting `mode` or defaults it on a narrower union.
84
+ *
85
+ * The `NonNullable` in that pin is objectui#4550's mark and is load-bearing:
86
+ * `NavigationConfig` no longer strips the authoring-side `undefined`, so
87
+ * `NavigationConfig['mode']` is `NavigationMode | undefined` and a bare
88
+ * equality would now fail for a reason unrelated to the drift being guarded.
89
+ * What stays pinned is the MEMBERSHIP — the seven modes this hook switches on
90
+ * are exactly the seven the exported union publishes.
91
+ */
92
+ export type NavigationMode = SpecNavigationMode;
45
93
  export interface UseNavigationOverlayOptions {
46
94
  /** The navigation configuration from the schema */
47
95
  navigation?: NavigationConfig;
@@ -5,25 +5,21 @@
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 type { OfflineStrategy, ConflictResolution, PersistStorage, EvictionPolicy, OfflineCacheConfigSchema, OfflineConfigSchema, SyncConfigSchema } from '@objectstack/spec/ui';
9
- import type { SpecAuthoredInput } from '../spec-input';
10
8
  /** Offline strategy determines how data is fetched when connectivity is limited. */
11
- export type { OfflineStrategy };
9
+ export type OfflineStrategy = 'cache_first' | 'network_first' | 'stale_while_revalidate' | 'network_only' | 'cache_only';
12
10
  /**
13
11
  * Conflict resolution strategy for sync operations.
14
12
  *
15
- * Renamed from `ConflictResolutionStrategy` — that name belongs to a DIFFERENT
16
- * spec export (`@objectstack/spec/api`: `error | priority | first-wins |
17
- * last-wins`, the metadata-merge policy). The spec's name for the union this
18
- * hook actually uses is `ConflictResolution`, so the fix was to take the spec's
19
- * own name and its binding at once, the same move `FieldGroup` →
20
- * `ObjectFieldGroup` made in objectui#3169.
13
+ * Deliberately NOT named `ConflictResolutionStrategy` — that name belongs to a
14
+ * different contract (`@objectstack/spec/api`: `error | priority | first-wins |
15
+ * last-wins`, the metadata-merge policy), and the confusion between the two is
16
+ * what objectui#3169 / objectstack#4115 cost.
21
17
  */
22
- export type { ConflictResolution };
18
+ export type ConflictResolution = 'manual' | 'client_wins' | 'server_wins' | 'last_write_wins';
23
19
  /** Persist storage backend. */
24
- export type PersistStorageType = PersistStorage;
20
+ export type PersistStorageType = 'sqlite' | 'indexeddb' | 'localstorage';
25
21
  /** Eviction policy for cache management. */
26
- export type EvictionPolicyType = EvictionPolicy;
22
+ export type EvictionPolicyType = 'lru' | 'lfu' | 'fifo';
27
23
  /** Sync state of the offline system. */
28
24
  export type SyncState = 'idle' | 'syncing' | 'error' | 'offline';
29
25
  /** A queued mutation waiting to be synced. */
@@ -35,37 +31,67 @@ export interface QueuedMutation {
35
31
  data?: Record<string, unknown>;
36
32
  }
37
33
  /**
38
- * Cache configuration — the AUTHORING side of the spec's
39
- * `OfflineCacheConfigSchema`.
34
+ * Cache configuration.
40
35
  *
41
- * `persistStorage` and `evictionPolicy` carry `.default()`s, so they are
42
- * optional to write and present after a parse. This hook is handed what an app
43
- * author wrote, hence the input side; see {@link SpecAuthoredInput}.
36
+ * `persistStorage` and `evictionPolicy` used to carry schema `.default()`s, so
37
+ * they stay optional to WRITE — this hook is handed what an app author wrote,
38
+ * which is why every key here is optional (the former `SpecAuthoredInput` /
39
+ * input-side reading of the retired schema).
44
40
  */
45
- export type OfflineCacheConfig = SpecAuthoredInput<typeof OfflineCacheConfigSchema>;
46
- /**
47
- * Sync configuration — the AUTHORING side of the spec's `SyncConfigSchema`.
48
- *
49
- * `strategy` and `conflictResolution` carry `.default()`s, so the output type
50
- * makes them required; this hook is handed what an app author wrote.
51
- */
52
- export type OfflineSyncConfig = SpecAuthoredInput<typeof SyncConfigSchema>;
41
+ export interface OfflineCacheConfig {
42
+ /** Maximum cache size. */
43
+ maxSize?: number;
44
+ /** Time-to-live for cached entries. */
45
+ ttl?: number;
46
+ /** Storage backend. Defaults to `indexeddb` when a cache is used. */
47
+ persistStorage?: PersistStorageType;
48
+ /** Eviction policy once `maxSize` is reached. Defaults to `lru`. */
49
+ evictionPolicy?: EvictionPolicyType;
50
+ }
51
+ /** Sync configuration — controls how queued mutations reach the server. */
52
+ export interface OfflineSyncConfig {
53
+ /** Fetch strategy for sync requests. */
54
+ strategy?: OfflineStrategy;
55
+ /** How to resolve a server/client conflict. */
56
+ conflictResolution?: ConflictResolution;
57
+ /** Delay between retry attempts, in ms. */
58
+ retryInterval?: number;
59
+ /** Maximum retry attempts before giving up. */
60
+ maxRetries?: number;
61
+ /** Mutations flushed per sync batch. */
62
+ batchSize?: number;
63
+ }
53
64
  /**
54
- * Top-level offline configuration — the AUTHORING side of the spec's
55
- * `OfflineConfigSchema`.
65
+ * Top-level offline configuration.
56
66
  *
57
67
  * This name stays with `@object-ui/react` (objectui#3156 / objectui#3159): the
58
68
  * `OfflineConfig` that used to sit in `@object-ui/types` was a service-worker
59
69
  * ROUTE cache and has been renamed `PWAOfflineConfig`, so there is no
60
- * cross-package clash left — this hook's config is the spec's concept, key for
61
- * key, and takes the spec's binding.
70
+ * cross-package clash.
62
71
  *
63
- * Input side, not `z.infer`, and here the reason is visible in the signature:
64
- * `useOffline(config: OfflineConfig = {})` defaults to the empty object, which
65
- * the output type — where `enabled`, `strategy` and `offlineIndicator` are all
66
- * required once their `.default()`s have run — would reject outright.
72
+ * Every key is optional, and that is load-bearing rather than incidental:
73
+ * `useOffline(config: OfflineConfig = {})` defaults to the empty object, so a
74
+ * shape that made `enabled` / `strategy` / `offlineIndicator` required — as the
75
+ * retired schema's OUTPUT side did, once its `.default()`s had run — would
76
+ * reject the hook's own default argument. {@link DEFAULTS} below applies the
77
+ * same values the schema defaults used to.
67
78
  */
68
- export type OfflineConfig = SpecAuthoredInput<typeof OfflineConfigSchema>;
79
+ export interface OfflineConfig {
80
+ /** Whether offline mode is active. Defaults to `true`. */
81
+ enabled?: boolean;
82
+ /** Fetch strategy. Defaults to `network_first`. */
83
+ strategy?: OfflineStrategy;
84
+ /** Client-side cache configuration. */
85
+ cache?: OfflineCacheConfig;
86
+ /** Sync behaviour for queued mutations. */
87
+ sync?: OfflineSyncConfig;
88
+ /** Whether to surface the offline indicator UI. Defaults to `true`. */
89
+ offlineIndicator?: boolean;
90
+ /** Message shown while offline. */
91
+ offlineMessage?: string;
92
+ /** Maximum queued mutations retained. Defaults to `100`. */
93
+ queueMaxSize?: number;
94
+ }
69
95
  /** Result returned by the useOffline hook. */
70
96
  export interface OfflineResult {
71
97
  /** Whether the browser is currently online. */
@@ -91,8 +117,8 @@ export interface OfflineResult {
91
117
  }
92
118
  /**
93
119
  * Hook for offline mode detection and sync queue management. Its config types
94
- * ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
95
- * (authoring side) — see {@link OfflineConfig}.
120
+ * are declared locally — see {@link OfflineConfig} for why they stopped being
121
+ * the spec's.
96
122
  *
97
123
  * @example
98
124
  * ```tsx
@@ -67,8 +67,8 @@ const DEFAULTS = {
67
67
  };
68
68
  /**
69
69
  * Hook for offline mode detection and sync queue management. Its config types
70
- * ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
71
- * (authoring side) — see {@link OfflineConfig}.
70
+ * are declared locally — see {@link OfflineConfig} for why they stopped being
71
+ * the spec's.
72
72
  *
73
73
  * @example
74
74
  * ```tsx
@@ -76,10 +76,10 @@ export function usePageAssignment(objectName, opts = {}) {
76
76
  if (!p)
77
77
  return false;
78
78
  // Only `pageType: 'record'` (or bare `type: 'record'`) is a
79
- // user-facing record-detail page. `pageType: 'record_detail'` is a
80
- // distinct concept used by the metadata designer page schemas
81
- // (`buildObjectDetailPageSchema`) and must NOT be picked up here —
82
- // otherwise opening any record would render the designer widget.
79
+ // user-facing record-detail page. Any other `pageType` — notably the
80
+ // designer-side `'record_detail'`, dropped from `PageType` in
81
+ // framework#2265 but still expressible in raw metadata — must NOT be
82
+ // picked up here, otherwise opening any record would render it.
83
83
  const pt = p.pageType ?? (p.type === 'record' ? 'record' : undefined);
84
84
  const isRecord = pt === 'record';
85
85
  if (!isRecord)
package/dist/index.d.ts CHANGED
@@ -6,12 +6,14 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  export * from './SchemaRenderer';
9
+ export * from './schema-input';
9
10
  export * from './hooks';
10
11
  export * from './context';
11
12
  export * from './LazyPluginLoader';
12
13
  export * from './spec-bridge';
13
14
  export * from './data-invalidation';
14
- export { resolveI18nLabel } from './utils/i18n';
15
+ export * from './element-data-source/ElementDataSourceGate';
16
+ export { resolveKeyedI18nLabel } from './utils/i18n';
15
17
  export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
16
18
  export type { WriteFieldError } from './utils/error-message';
17
19
  export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type I18nConfig, type I18nProviderProps, type TranslationKeys, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from '@object-ui/i18n';
package/dist/index.js CHANGED
@@ -6,13 +6,17 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  export * from './SchemaRenderer';
9
+ export * from './schema-input';
9
10
  export * from './hooks'; // will be empty for now
10
11
  export * from './context'; // will be empty for now
11
12
  export * from './LazyPluginLoader';
12
13
  export * from './spec-bridge';
13
14
  export * from './data-invalidation';
15
+ // PageComponentSchema.dataSource — mapping the spec's per-element data binding
16
+ // onto the schema keys each object-bound block reads (objectstack#6953).
17
+ export * from './element-data-source/ElementDataSourceGate';
14
18
  // i18n utilities
15
- export { resolveI18nLabel } from './utils/i18n';
19
+ export { resolveKeyedI18nLabel } from './utils/i18n';
16
20
  // Write-error surfacing utilities (shared by drag-write plugins so a failed
17
21
  // PATCH — e.g. an RLS 403 — is never silently swallowed).
18
22
  export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
@@ -0,0 +1,31 @@
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
+ import type { BaseSchema } from '@object-ui/types';
9
+ import type { SchemaRendererProps } from './SchemaRenderer';
10
+ /**
11
+ * Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
12
+ *
13
+ * `@object-ui/types` declares `SchemaNode` as
14
+ * `BaseSchema | string | number | boolean | null | undefined`, and a lot of
15
+ * metadata plumbing (page regions, header-bar actions, detail tabs, view
16
+ * configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
17
+ * `number` / `boolean` members — nobody should be invited to author them — so
18
+ * forwarding such a value needs one honest step in between.
19
+ *
20
+ * This is that step, and it is a TOTAL function rather than a cast: it maps the
21
+ * two primitive members onto their text form, which is precisely what the
22
+ * renderer's own defensive branch does with them. So it changes no behaviour and
23
+ * tells no lie — the value a caller forwards renders identically whether or not
24
+ * it passes through here.
25
+ *
26
+ * It exists because the two competing repo-wide `SchemaNode` spellings
27
+ * (`@object-ui/core`'s interface vs `@object-ui/types`' union) have not been
28
+ * reconciled; that reconciliation is tracked separately. When it lands, the
29
+ * call sites using this can go back to forwarding directly.
30
+ */
31
+ export declare function toRenderableSchema(node: BaseSchema | string | number | boolean | null | undefined): SchemaRendererProps['schema'];
@@ -0,0 +1,31 @@
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
+ * Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
10
+ *
11
+ * `@object-ui/types` declares `SchemaNode` as
12
+ * `BaseSchema | string | number | boolean | null | undefined`, and a lot of
13
+ * metadata plumbing (page regions, header-bar actions, detail tabs, view
14
+ * configs) is typed with it. `SchemaRenderer` deliberately does NOT declare the
15
+ * `number` / `boolean` members — nobody should be invited to author them — so
16
+ * forwarding such a value needs one honest step in between.
17
+ *
18
+ * This is that step, and it is a TOTAL function rather than a cast: it maps the
19
+ * two primitive members onto their text form, which is precisely what the
20
+ * renderer's own defensive branch does with them. So it changes no behaviour and
21
+ * tells no lie — the value a caller forwards renders identically whether or not
22
+ * it passes through here.
23
+ *
24
+ * It exists because the two competing repo-wide `SchemaNode` spellings
25
+ * (`@object-ui/core`'s interface vs `@object-ui/types`' union) have not been
26
+ * reconciled; that reconciliation is tracked separately. When it lands, the
27
+ * call sites using this can go back to forwarding directly.
28
+ */
29
+ export function toRenderableSchema(node) {
30
+ return typeof node === 'number' || typeof node === 'boolean' ? String(node) : node;
31
+ }
@@ -5,18 +5,24 @@
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 type { SchemaNode } from '@object-ui/core';
8
+ import type { BaseSchema } from '@object-ui/types';
9
9
  import type { BridgeContext, BridgeFn } from './types';
10
10
  export declare class SpecBridge {
11
11
  private bridges;
12
12
  private context;
13
13
  constructor(context?: BridgeContext);
14
14
  register(specType: string, bridge: BridgeFn): void;
15
- /** Transform a spec schema into a SchemaNode tree */
16
- transform(specType: string, spec: any): SchemaNode;
15
+ /**
16
+ * Transform a spec schema into a schema node tree.
17
+ *
18
+ * Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
19
+ * the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
20
+ * bridge always produces an object; the union only made callers narrow.
21
+ */
22
+ transform(specType: string, spec: any): BaseSchema;
17
23
  /** Transform a ListView spec */
18
- transformListView(spec: any): SchemaNode;
24
+ transformListView(spec: any): BaseSchema;
19
25
  /** Transform a FormView spec */
20
- transformFormView(spec: any): SchemaNode;
26
+ transformFormView(spec: any): BaseSchema;
21
27
  updateContext(ctx: Partial<BridgeContext>): void;
22
28
  }
@@ -28,7 +28,13 @@ export class SpecBridge {
28
28
  register(specType, bridge) {
29
29
  this.bridges.set(specType, bridge);
30
30
  }
31
- /** Transform a spec schema into a SchemaNode tree */
31
+ /**
32
+ * Transform a spec schema into a schema node tree.
33
+ *
34
+ * Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
35
+ * the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
36
+ * bridge always produces an object; the union only made callers narrow.
37
+ */
32
38
  transform(specType, spec) {
33
39
  const bridge = this.bridges.get(specType);
34
40
  if (!bridge) {