@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
@@ -38,21 +38,113 @@ function mapColumn(col) {
38
38
  mapped.action = col.action;
39
39
  return mapped;
40
40
  }
41
+ /**
42
+ * The spec's five row heights collapsed onto the renderer's three densities.
43
+ *
44
+ * `Record<RowHeight, …>` on purpose (objectui#4352): the key set is the spec's,
45
+ * so a row height added upstream fails the build here instead of silently
46
+ * arriving with no density. The table used to carry four more keys —
47
+ * `comfortable`, `spacious`, `small`, `large` — which `RowHeightSchema` does
48
+ * not admit, so no spec-valid list view could ever reach them; they only
49
+ * survived because the parameter was widened back to `string` and the fixture
50
+ * asserting three of them compiled against nothing. Deleting them is AGENTS.md
51
+ * #0.1: a renderer-side dialect for off-spec metadata is a second de-facto
52
+ * contract, and one strict contract beats N. An off-spec `rowHeight` now falls
53
+ * through to `undefined` — the producer is where it gets fixed.
54
+ */
55
+ const ROW_HEIGHT_TO_DENSITY = {
56
+ compact: 'compact',
57
+ short: 'compact',
58
+ medium: 'comfortable',
59
+ tall: 'spacious',
60
+ extra_tall: 'spacious',
61
+ };
62
+ /**
63
+ * Runtime reader for {@link ROW_HEIGHT_TO_DENSITY} — the five spec row heights
64
+ * and **nothing else**.
65
+ *
66
+ * `hasOwnProperty`, not a bare index (objectui#4442): the table is a plain
67
+ * object literal, so indexing it with an unchecked key reaches
68
+ * `Object.prototype`. The parameter is typed `RowHeight`, but the boundary a
69
+ * host's stored view definition actually crosses is `SpecBridge.transformListView`,
70
+ * whose parameter is `any` — so `rowHeight: 'toString'` used to come back as
71
+ * `Object.prototype.toString`, a FUNCTION returned from a signature that
72
+ * promises three strings or nothing. `bridgeListView` then writes the key under
73
+ * `if (density)`, and a function is truthy, so it landed on the SchemaNode.
74
+ *
75
+ * This is the same guard `@object-ui/core`'s `rowHeightToDensityMode` grew in
76
+ * objectui#4440, and the repo's existing convention (`freeze-schema.ts:135`,
77
+ * `metadata-admin/predicate.ts:305`, six more). Both rowHeight surfaces now
78
+ * abstain identically on every off-spec spelling; the agreement is pinned by
79
+ * `__tests__/RowHeightDensityAgreement.test.ts`.
80
+ *
81
+ * `typeof rowHeight !== 'string'`, not `!rowHeight` (objectui#4459): the
82
+ * truthiness guard this replaces rejected only FALSY values, so every truthy
83
+ * non-string walked past it into a lookup that coerces its key — both
84
+ * `hasOwnProperty.call` and the index run `String(...)`. `['compact']`, a boxed
85
+ * `String('compact')` and `{ toString: () => 'compact' }` therefore each
86
+ * selected a real density, where core's twin abstained
87
+ * (`packages/core/src/utils/normalize-list-view.ts:83`, which has opened with
88
+ * the type guard since #4440). Note the direction against #4442: that leak produced a
89
+ * FUNCTION, visibly wrong to everything downstream; this one produced a
90
+ * legitimate-looking `'compact'` that nothing can tell apart from an authored
91
+ * value. The type guard subsumes the old one — `undefined`, `null`, `0` and
92
+ * `false` are all non-strings — and `''` keeps its answer while changing route:
93
+ * a string, so it passes here and is refused by `hasOwnProperty` one line down.
94
+ */
41
95
  function mapDensity(rowHeight) {
42
- if (!rowHeight)
96
+ if (typeof rowHeight !== 'string')
43
97
  return undefined;
44
- const map = {
45
- compact: 'compact',
46
- short: 'compact',
47
- comfortable: 'comfortable',
48
- spacious: 'spacious',
49
- small: 'compact',
50
- medium: 'comfortable',
51
- large: 'spacious',
52
- tall: 'spacious',
53
- extra_tall: 'spacious',
54
- };
55
- return map[rowHeight];
98
+ if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) {
99
+ return undefined;
100
+ }
101
+ return ROW_HEIGHT_TO_DENSITY[rowHeight];
102
+ }
103
+ /**
104
+ * The spec's own parse-time lift, applied where `parse` cannot reach
105
+ * (objectui#4585).
106
+ *
107
+ * `@objectstack/spec` accepts BOTH spellings of `view.exportOptions` and lifts
108
+ * the legacy bare format array to the object form at parse (objectstack#8010,
109
+ * `ui/view.zod.ts`):
110
+ *
111
+ * z.array(ListViewExportFormatSchema).transform((formats) => ({ formats }))
112
+ *
113
+ * That transform never runs on this path. The bridge's input is a TypeScript
114
+ * type, not a parsed value — there is no `parse`/`safeParse` anywhere under
115
+ * `spec-bridge/` — so a host that parses first hands over the object form while
116
+ * a host that forwards raw stored metadata hands over whatever was authored,
117
+ * and nothing here can tell them apart. The legacy array therefore used to
118
+ * reach the `object-grid` node verbatim, where `ObjectGrid` reads
119
+ * `exportOptions.formats` and only that: `.formats` on an array is `undefined`,
120
+ * the renderer's `['csv', 'json']` default won, and the view's declared formats
121
+ * were dropped with no error and no console line — the export button still
122
+ * showed, because a non-empty array is truthy.
123
+ *
124
+ * This mirrors that transform and NOTHING more: the same contract applied one
125
+ * layer out, not a second de-facto one (AGENTS.md #0.1 — the consumer-side
126
+ * `Array.isArray` fallback in the renderer is what that rule forbids). Hence:
127
+ *
128
+ * - the object form passes through by reference, unread and unrewritten;
129
+ * - an empty array lifts to `{ formats: [] }`, exactly as the spec's transform
130
+ * does — its `z.array()` carries no `.min(1)`, so `[]` is a legal input that
131
+ * wraps rather than defaults. ObjectGrid then offers no format and hides the
132
+ * export button: "declared zero formats", read literally;
133
+ * - a `'pdf'` stored before its retirement is carried, not filtered. The spec
134
+ * REFUSES `'pdf'` at parse with a migration prescription (objectstack#8010;
135
+ * PDF export itself was declined as objectstack#1301 NOT_PLANNED) — it does
136
+ * not silently drop the value, so neither may this. Such a format dies
137
+ * downstream in ObjectGrid's format-AGNOSTIC menu filter, kept deliberately
138
+ * at objectui#4535 for metadata predating `os migrate meta --from 16`.
139
+ */
140
+ function liftExportOptions(exportOptions) {
141
+ if (!Array.isArray(exportOptions))
142
+ return exportOptions;
143
+ // The pinned `@objectstack/spec@17.0.0-rc.6` still admits `'pdf'` as an array
144
+ // member (the enum narrowing arrives with the pin bump), so the element type
145
+ // here is wider than the renderer-side `ListViewExportFormat`. Narrowing it by
146
+ // filtering would invent precisely the silent drop the spec declines to do.
147
+ return { formats: exportOptions };
56
148
  }
57
149
  /** Transforms a ListView spec into a DataTable SchemaNode */
58
150
  export const bridgeListView = (spec, _context) => {
@@ -116,7 +208,7 @@ export const bridgeListView = (spec, _context) => {
116
208
  if (spec.inlineEdit != null)
117
209
  node.inlineEdit = spec.inlineEdit;
118
210
  if (spec.exportOptions)
119
- node.exportOptions = spec.exportOptions;
211
+ node.exportOptions = liftExportOptions(spec.exportOptions);
120
212
  if (spec.emptyState)
121
213
  node.emptyState = spec.emptyState;
122
214
  if (spec.userActions)
@@ -5,7 +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 type { SchemaNode } from '@object-ui/core';
8
+ import type { BaseSchema } from '@object-ui/types';
9
9
  /** Context passed to all bridge functions */
10
10
  export interface BridgeContext {
11
11
  /** Current user context for permission checks */
@@ -30,5 +30,26 @@ export interface ObjectDefLite {
30
30
  }>;
31
31
  }>;
32
32
  }
33
- /** A bridge function transforms a spec schema into a SchemaNode tree */
34
- export type BridgeFn<T = any> = (spec: T, context: BridgeContext) => SchemaNode;
33
+ /**
34
+ * A bridge function transforms a spec schema into a schema node tree.
35
+ *
36
+ * Returns **`BaseSchema`**, not `SchemaNode` (objectui#4580, ruling Q4-B).
37
+ * Once core's `SchemaNode` became `@object-ui/types`' union
38
+ * (`BaseSchema | string | number | boolean | null | undefined`), declaring the
39
+ * return as `SchemaNode` made every caller re-ask a question no bridge has ever
40
+ * answered "yes" to: *is this a bare string / null?* Both shipped bridges end
41
+ * in a single `return node` on an object literal — `bridges/list-view.ts:226`
42
+ * and `bridges/form-view.ts:195` — so the wider declaration described nothing
43
+ * real while forcing a narrowing at every read.
44
+ *
45
+ * That cost was measured, not assumed: round 1 of this card recorded **272**
46
+ * mechanical errors across five spec-bridge suites, all of them TS18049
47
+ * (`'node' is possibly 'null' or 'undefined'`) and TS2339
48
+ * (`Property 'formType' does not exist on type
49
+ * 'string | number | boolean | BaseSchema'`) — the union being destructured by
50
+ * tests reading properties off a node the bridge always produces.
51
+ *
52
+ * This is a type ANNOTATION only: no runtime behaviour changes, and the emitted
53
+ * JavaScript is byte-identical (proven by bundle sha256 on this PR).
54
+ */
55
+ export type BridgeFn<T = any> = (spec: T, context: BridgeContext) => BaseSchema;
@@ -1,10 +1,45 @@
1
+ import type { KeyedI18nLabel } from '@object-ui/types';
1
2
  /**
2
- * Resolves an I18nLabel to a plain string.
3
- * I18nLabel can be either a string or an object { key, defaultValue?, params? }.
4
- * When it's an object, we return the defaultValue or the key as fallback.
3
+ * Resolves objectui's KEYED i18n label to a plain string.
4
+ *
5
+ * The keyed form is `{ key, defaultValue?, params? }` — a reference INTO a
6
+ * translation bundle. This package has no `t()`, so it resolves the reference
7
+ * as far as it can without one: `defaultValue`, else the key itself. (The
8
+ * app-shell twin of this function takes a `t` and resolves the key properly.)
9
+ *
10
+ * ## Why this is not called `resolveI18nLabel` (objectui#4167)
11
+ *
12
+ * `@objectstack/spec` 17.0.0-rc.6 publishes a function of that exact name from
13
+ * `@objectstack/spec/ui`, over a DIFFERENT vocabulary: the INLINE LOCALE MAP
14
+ * (`{ en: 'Owner', 'zh-CN': '负责人' }`), resolved against a BCP-47 locale
15
+ * (`resolveI18nLabel(label, locale)`). Two resolvers, one name, and neither
16
+ * accepts the other's shape — which objectstack#4115 calls a planted premise
17
+ * rather than a naming nit, because the next agent reads the name and builds
18
+ * on it.
19
+ *
20
+ * rc.6 is also what made the clash dangerous rather than merely untidy. It
21
+ * widened the spec's `I18nLabel` from `string` to
22
+ * `string | Record< string, string >`, so the SAME authored value can now reach
23
+ * either function — and each answers wrongly for the other's input, silently:
24
+ *
25
+ * this one, given `{ en: 'Owner' }` → `undefined` (no `key`, no
26
+ * `defaultValue`) → renders empty;
27
+ * the spec's, given `{ key: 'nav.home' }` → treats `key`/`defaultValue`/
28
+ * `params` as locale tags and picks
29
+ * one of them as the display text.
30
+ *
31
+ * Neither is a type error at the call site that matters, and both look like a
32
+ * working render until someone reads the output. PR #4169 met this head-on and
33
+ * aliased the spec's import as `resolveInlineI18nLabel` in five files, with
34
+ * hand-written comments explaining the clash at two of them — a review
35
+ * convention, which is exactly what objectstack#4115 exists to replace with a
36
+ * rule. `Keyed` is the counterpart of that `Inline`: the name now says which
37
+ * vocabulary it resolves, at every call site, with no comment required.
38
+ *
39
+ * The keyed shape itself is now named too — `KeyedI18nLabel` in
40
+ * `@object-ui/types` (#4581) — so `BaseSchema.ariaLabel`, this parameter and
41
+ * the layout twin all state one type instead of three copies of one object
42
+ * literal. The `Inline`/`Keyed` split above is the naming half of #4167; the
43
+ * named shape is the declaration half.
5
44
  */
6
- export declare function resolveI18nLabel(label: string | {
7
- key: string;
8
- defaultValue?: string;
9
- params?: Record<string, any>;
10
- } | undefined): string | undefined;
45
+ export declare function resolveKeyedI18nLabel(label: string | KeyedI18nLabel | undefined): string | undefined;
@@ -1,9 +1,47 @@
1
1
  /**
2
- * Resolves an I18nLabel to a plain string.
3
- * I18nLabel can be either a string or an object { key, defaultValue?, params? }.
4
- * When it's an object, we return the defaultValue or the key as fallback.
2
+ * Resolves objectui's KEYED i18n label to a plain string.
3
+ *
4
+ * The keyed form is `{ key, defaultValue?, params? }` — a reference INTO a
5
+ * translation bundle. This package has no `t()`, so it resolves the reference
6
+ * as far as it can without one: `defaultValue`, else the key itself. (The
7
+ * app-shell twin of this function takes a `t` and resolves the key properly.)
8
+ *
9
+ * ## Why this is not called `resolveI18nLabel` (objectui#4167)
10
+ *
11
+ * `@objectstack/spec` 17.0.0-rc.6 publishes a function of that exact name from
12
+ * `@objectstack/spec/ui`, over a DIFFERENT vocabulary: the INLINE LOCALE MAP
13
+ * (`{ en: 'Owner', 'zh-CN': '负责人' }`), resolved against a BCP-47 locale
14
+ * (`resolveI18nLabel(label, locale)`). Two resolvers, one name, and neither
15
+ * accepts the other's shape — which objectstack#4115 calls a planted premise
16
+ * rather than a naming nit, because the next agent reads the name and builds
17
+ * on it.
18
+ *
19
+ * rc.6 is also what made the clash dangerous rather than merely untidy. It
20
+ * widened the spec's `I18nLabel` from `string` to
21
+ * `string | Record< string, string >`, so the SAME authored value can now reach
22
+ * either function — and each answers wrongly for the other's input, silently:
23
+ *
24
+ * this one, given `{ en: 'Owner' }` → `undefined` (no `key`, no
25
+ * `defaultValue`) → renders empty;
26
+ * the spec's, given `{ key: 'nav.home' }` → treats `key`/`defaultValue`/
27
+ * `params` as locale tags and picks
28
+ * one of them as the display text.
29
+ *
30
+ * Neither is a type error at the call site that matters, and both look like a
31
+ * working render until someone reads the output. PR #4169 met this head-on and
32
+ * aliased the spec's import as `resolveInlineI18nLabel` in five files, with
33
+ * hand-written comments explaining the clash at two of them — a review
34
+ * convention, which is exactly what objectstack#4115 exists to replace with a
35
+ * rule. `Keyed` is the counterpart of that `Inline`: the name now says which
36
+ * vocabulary it resolves, at every call site, with no comment required.
37
+ *
38
+ * The keyed shape itself is now named too — `KeyedI18nLabel` in
39
+ * `@object-ui/types` (#4581) — so `BaseSchema.ariaLabel`, this parameter and
40
+ * the layout twin all state one type instead of three copies of one object
41
+ * literal. The `Inline`/`Keyed` split above is the naming half of #4167; the
42
+ * named shape is the declaration half.
5
43
  */
6
- export function resolveI18nLabel(label) {
44
+ export function resolveKeyedI18nLabel(label) {
7
45
  if (label === undefined || label === null)
8
46
  return undefined;
9
47
  if (typeof label === 'string')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "17.2.0",
3
+ "version": "17.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "React bindings and SchemaRenderer component for Object UI",
@@ -24,20 +24,20 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^17.0.0-rc.1",
28
- "react-hook-form": "^7.83.0",
29
- "@object-ui/core": "17.2.0",
30
- "@object-ui/data-objectstack": "17.2.0",
31
- "@object-ui/i18n": "17.2.0",
32
- "@object-ui/types": "17.2.0"
27
+ "@objectstack/spec": "^17.0.0-rc.6",
28
+ "react-hook-form": "^7.84.0",
29
+ "@object-ui/core": "17.5.0",
30
+ "@object-ui/data-objectstack": "17.5.0",
31
+ "@object-ui/i18n": "17.5.0",
32
+ "@object-ui/types": "17.5.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.0.0 || ^19.0.0",
36
36
  "react-dom": "^18.0.0 || ^19.0.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/react": "19.2.17",
40
- "@types/react-dom": "19.2.3",
39
+ "@types/react": "19.2.18",
40
+ "@types/react-dom": "19.2.4",
41
41
  "typescript": "^6.0.3"
42
42
  },
43
43
  "keywords": [
@@ -65,7 +65,7 @@
65
65
  "scripts": {
66
66
  "build": "tsc",
67
67
  "test": "vitest run",
68
- "type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
68
+ "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
69
69
  "lint": "eslint ."
70
70
  }
71
71
  }