@object-ui/core 16.1.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -1,51 +1,54 @@
1
1
  /**
2
- * managedBy — the single source of truth for interpreting an object's
3
- * lifecycle bucket (`ObjectSchema.managedBy`) into CRUD affordances.
2
+ * managedBy — the UI end of the platform's `managedBy` → CRUD affordance
3
+ * contract (ADR-0103).
4
4
  *
5
- * UI-side mirror of the framework's `resolveCrudAffordances()`
6
- * (`@objectstack/spec/data/object.zod.ts`, ADR-0103). Lives in `@object-ui/core`
7
- * — React-free and reachable by every UI package (app-shell, plugin-detail,
8
- * plugin-form, plugin-grid) — so the bucket logic is defined ONCE instead of
9
- * hand-mirrored in each. Components ask "should I show New / Import / Edit /
10
- * Delete / Export?" and get an answer that tracks the object's lifecycle without
11
- * special-casing `sys_*` names.
5
+ * ## What is spec-owned here, and what is not (objectstack#4115)
12
6
  *
13
- * The bucket UNION type lives in `@object-ui/types` (`ManagedByBucket`) so the
14
- * schema field can reference it; this module re-exports it for convenience.
15
- * Only the affordance BOOLEANS live here — all translated copy (badge variants,
16
- * empty-state messages) stays in `@object-ui/app-shell`.
7
+ * The bucket logic is **the spec's**, and this module now says so in code
8
+ * rather than in a comment. `CrudAffordances` / `RowCrudPredicates` are
9
+ * re-exported from `@objectstack/spec/data`, and the bucket resolution itself
10
+ * delegates to the spec's `resolveCrudAffordances()`.
11
+ *
12
+ * This module used to describe itself as a "UI-side mirror" of that function
13
+ * and carry its own `DEFAULTS` table — a line-for-line copy of the spec's
14
+ * `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of its override parser. A faithful
15
+ * copy passes every value comparison and every behavioural test, so nothing
16
+ * would have caught the release in which the spec re-buckets an object class;
17
+ * reference identity is the only check that distinguishes a re-export from a
18
+ * fork (objectui#3003).
19
+ *
20
+ * What is genuinely objectui's and keeps a local name:
21
+ * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
+ * INTERSECTED with the server-resolved effective API operation set (#3391), so
23
+ * the UI never offers a button the server would 405. The spec's function has
24
+ * no such notion; naming ours after it was the misdescription this burn-down
25
+ * removes.
26
+ *
27
+ * Lives in `@object-ui/core` — React-free and reachable by every UI package
28
+ * (app-shell, plugin-detail, plugin-form, plugin-grid) — so the intersection is
29
+ * defined ONCE instead of hand-mirrored in each. The bucket UNION type lives in
30
+ * `@object-ui/types` (`ManagedByBucket`) and is re-exported here for
31
+ * convenience. All translated copy (badge variants, empty-state messages) stays
32
+ * in `@object-ui/app-shell`.
17
33
  */
34
+ import { type CrudAffordances, type RowCrudPredicates } from '@objectstack/spec/data';
18
35
  export type { ManagedByBucket } from '@object-ui/types';
19
- export interface CrudAffordances {
20
- /** Generic "New" button for single-record creation. */
21
- create: boolean;
22
- /** CSV bulk-import wizard. */
23
- import: boolean;
24
- /** Inline + form editing of existing rows. */
25
- edit: boolean;
26
- /** Row-level + bulk delete. */
27
- delete: boolean;
28
- /** CSV / clipboard export. */
29
- exportCsv: boolean;
30
- /**
31
- * Per-record CEL predicates for the built-in row Edit/Delete actions,
32
- * present only when `userActions.edit` / `delete` used the object form
33
- * (objectui#2614). Carried through as authored for row renderers to evaluate;
34
- * they never affect the object-level booleans above.
35
- */
36
- editPredicates?: RowCrudPredicates;
37
- deletePredicates?: RowCrudPredicates;
38
- }
39
- /** Per-record predicates from the #2614 object form of a userActions flag. */
40
- export interface RowCrudPredicates {
41
- visibleWhen?: unknown;
42
- disabledWhen?: unknown;
43
- }
36
+ /**
37
+ * The resolved CRUD affordance matrix and its per-record predicate envelope
38
+ * are **spec-owned** (`@objectstack/spec/data`, ADR-0103) and re-exported here,
39
+ * not re-declared.
40
+ *
41
+ * Note this also TIGHTENS the predicate types: the local copies typed
42
+ * `visibleWhen` / `disabledWhen` as `unknown`, while the spec types them as
43
+ * `Expression | ExpressionInput` — the authored CEL shorthand or its envelope.
44
+ * The local `unknown` was imprecision, not a deliberate dialect.
45
+ */
46
+ export type { CrudAffordances, RowCrudPredicates };
44
47
  /** `edit`/`delete` accept a bare boolean or the #2614 object form. */
45
48
  export type UserActionOverride = boolean | {
46
49
  enabled?: boolean;
47
- visibleWhen?: unknown;
48
- disabledWhen?: unknown;
50
+ visibleWhen?: RowCrudPredicates['visibleWhen'];
51
+ disabledWhen?: RowCrudPredicates['disabledWhen'];
49
52
  };
50
53
  export interface UserActionsOverride {
51
54
  create?: boolean;
@@ -63,11 +66,14 @@ export interface SchemaLike {
63
66
  * the given bucket default, surfacing any per-record predicates alongside.
64
67
  *
65
68
  * THE single parser for the `userActions.{edit,delete}` override shape — every
66
- * UI package (grid row affordances, related-list row predicates, this module's
67
- * own `resolveCrudAffordances`) routes through here instead of re-implementing
68
- * the boolean/object-form parse locally. `base` is the bucket default an
69
- * omitted `enabled` falls back to (callers that only gate on an explicit
70
- * opt-out pass `true`; predicate extraction is independent of `base`).
69
+ * UI package (grid row affordances, related-list row predicates) routes through
70
+ * here instead of re-implementing the boolean/object-form parse locally. The
71
+ * spec keeps its equivalent (`normalizeRowCrudOverride`) module-private, so
72
+ * this stays a local declaration — under a name the spec does not own.
73
+ *
74
+ * `base` is the bucket default an omitted `enabled` falls back to (callers that
75
+ * only gate on an explicit opt-out pass `true`; predicate extraction is
76
+ * independent of `base`).
71
77
  */
72
78
  export declare function normalizeUserAction(v: UserActionOverride | undefined | null, base: boolean): {
73
79
  enabled: boolean;
@@ -81,14 +87,47 @@ export declare function normalizeUserAction(v: UserActionOverride | undefined |
81
87
  * {@link normalizeUserAction} so the parse lives in exactly one place.
82
88
  */
83
89
  export declare function userActionPredicates(v: UserActionOverride | undefined | null): RowCrudPredicates | undefined;
84
- /** Resolve the effective CRUD affordances for an object schema. */
85
- export declare function resolveCrudAffordances(obj: SchemaLike | null | undefined): CrudAffordances;
90
+ /**
91
+ * Resolve the CRUD affordances a UI surface should expose for an object —
92
+ * the spec's object-level matrix, INTERSECTED with what the server will
93
+ * actually accept.
94
+ *
95
+ * The bucket/`userActions` half is **not computed here**: it is delegated to
96
+ * `resolveCrudAffordances()` from `@objectstack/spec/data` (ADR-0103), so the
97
+ * bucket table has exactly one definition on the platform. Only the
98
+ * intersection below is objectui's, which is why this function no longer
99
+ * carries the spec's name (objectstack#4115).
100
+ *
101
+ * @param obj The object schema (managedBy bucket + userActions overrides).
102
+ * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
103
+ * set for this object (from `/me/permissions` `apiOperations`, #3391). When
104
+ * provided, each affordance bit is ANDed with the corresponding API operation
105
+ * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
106
+ * so the UI never shows a button the server would 405. Passing `undefined`
107
+ * (old backend / no effective set) leaves the affordances untouched —
108
+ * backward-compatible. An empty array means "expose nothing" → all bits off.
109
+ */
110
+ export declare function resolveEffectiveCrudAffordances(obj: SchemaLike | null | undefined, effectiveApiOperations?: readonly string[] | null): CrudAffordances;
86
111
  /** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
87
112
  export declare function isWriteOptedIn(v: UserActionOverride | undefined | null): boolean;
88
113
  /**
89
- * ADR-0103 — a `system`-bucket object that opened ANY write via `userActions`
90
- * is admin/user-writable DATA, not an engine-owned monitoring surface. Used to
91
- * pick the writable-system badge/empty-state copy.
114
+ * True for a platform-defined schema whose DATA is admin/user-writable — the
115
+ * `system-data` bucket. Used to pick the writable-system badge/empty-state copy
116
+ * instead of the engine-owned "read-only monitoring surface" copy.
117
+ *
118
+ * **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split
119
+ * this had to RECOVER the distinction from `userActions`: `system` doubled as
120
+ * both the engine-owned default and the writable set, so "did it open any
121
+ * write?" was the only way to tell a Notification Preferences grid from an
122
+ * automation-run log. v17 retired that overload — the writable half is now the
123
+ * `system-data` bucket (writable by DEFAULT) and the engine-owned half is
124
+ * `engine-owned` — so the bucket answers the question directly and the
125
+ * `userActions` probe would only re-derive what the value already states.
126
+ *
127
+ * The narrowing case is deliberately still `true`: a `system-data` grid that
128
+ * narrows to edit-only is still platform-schema/user-data and should read as
129
+ * such. The spec refuses `system-data` on an object that narrows away every
130
+ * write, so there is no "writable bucket with no writes" shape to worry about.
92
131
  */
93
132
  export declare function isSystemWritable(obj: SchemaLike | null | undefined): boolean;
94
133
  /**
@@ -97,5 +136,12 @@ export declare function isSystemWritable(obj: SchemaLike | null | undefined): bo
97
136
  * `append-only` / `better-auth` objects resolve to `false` unless they opened
98
137
  * `userActions.edit`. (Per-record predicates gate row action buttons, not
99
138
  * object-level editability, so only the resolved boolean matters here.)
139
+ *
140
+ * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
141
+ * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
142
+ * provided, inline-edit is additionally ANDed with the server allowing
143
+ * `update`, so a record page never offers double-click/pencil editing the
144
+ * server would 405. `undefined` (old backend / unrestricted) leaves the
145
+ * bucket affordance untouched.
100
146
  */
101
- export declare function isObjectInlineEditable(obj: SchemaLike | null | undefined): boolean;
147
+ export declare function isObjectInlineEditable(obj: SchemaLike | null | undefined, effectiveApiOperations?: readonly string[] | null): boolean;
@@ -1,38 +1,50 @@
1
1
  /**
2
- * managedBy — the single source of truth for interpreting an object's
3
- * lifecycle bucket (`ObjectSchema.managedBy`) into CRUD affordances.
2
+ * managedBy — the UI end of the platform's `managedBy` → CRUD affordance
3
+ * contract (ADR-0103).
4
4
  *
5
- * UI-side mirror of the framework's `resolveCrudAffordances()`
6
- * (`@objectstack/spec/data/object.zod.ts`, ADR-0103). Lives in `@object-ui/core`
7
- * — React-free and reachable by every UI package (app-shell, plugin-detail,
8
- * plugin-form, plugin-grid) — so the bucket logic is defined ONCE instead of
9
- * hand-mirrored in each. Components ask "should I show New / Import / Edit /
10
- * Delete / Export?" and get an answer that tracks the object's lifecycle without
11
- * special-casing `sys_*` names.
5
+ * ## What is spec-owned here, and what is not (objectstack#4115)
12
6
  *
13
- * The bucket UNION type lives in `@object-ui/types` (`ManagedByBucket`) so the
14
- * schema field can reference it; this module re-exports it for convenience.
15
- * Only the affordance BOOLEANS live here — all translated copy (badge variants,
16
- * empty-state messages) stays in `@object-ui/app-shell`.
7
+ * The bucket logic is **the spec's**, and this module now says so in code
8
+ * rather than in a comment. `CrudAffordances` / `RowCrudPredicates` are
9
+ * re-exported from `@objectstack/spec/data`, and the bucket resolution itself
10
+ * delegates to the spec's `resolveCrudAffordances()`.
11
+ *
12
+ * This module used to describe itself as a "UI-side mirror" of that function
13
+ * and carry its own `DEFAULTS` table — a line-for-line copy of the spec's
14
+ * `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of its override parser. A faithful
15
+ * copy passes every value comparison and every behavioural test, so nothing
16
+ * would have caught the release in which the spec re-buckets an object class;
17
+ * reference identity is the only check that distinguishes a re-export from a
18
+ * fork (objectui#3003).
19
+ *
20
+ * What is genuinely objectui's and keeps a local name:
21
+ * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
+ * INTERSECTED with the server-resolved effective API operation set (#3391), so
23
+ * the UI never offers a button the server would 405. The spec's function has
24
+ * no such notion; naming ours after it was the misdescription this burn-down
25
+ * removes.
26
+ *
27
+ * Lives in `@object-ui/core` — React-free and reachable by every UI package
28
+ * (app-shell, plugin-detail, plugin-form, plugin-grid) — so the intersection is
29
+ * defined ONCE instead of hand-mirrored in each. The bucket UNION type lives in
30
+ * `@object-ui/types` (`ManagedByBucket`) and is re-exported here for
31
+ * convenience. All translated copy (badge variants, empty-state messages) stays
32
+ * in `@object-ui/app-shell`.
17
33
  */
18
- const DEFAULTS = {
19
- platform: { create: true, import: true, edit: true, delete: true, exportCsv: true },
20
- config: { create: true, import: false, edit: true, delete: true, exportCsv: true },
21
- system: { create: false, import: false, edit: false, delete: false, exportCsv: true },
22
- 'engine-owned': { create: false, import: false, edit: false, delete: false, exportCsv: true },
23
- 'append-only': { create: false, import: false, edit: false, delete: false, exportCsv: true },
24
- 'better-auth': { create: false, import: false, edit: false, delete: false, exportCsv: true },
25
- };
34
+ import { resolveCrudAffordances as resolveSpecCrudAffordances, } from '@objectstack/spec/data';
26
35
  /**
27
36
  * Collapse an `edit`/`delete` override (boolean or #2614 object form) onto
28
37
  * the given bucket default, surfacing any per-record predicates alongside.
29
38
  *
30
39
  * THE single parser for the `userActions.{edit,delete}` override shape — every
31
- * UI package (grid row affordances, related-list row predicates, this module's
32
- * own `resolveCrudAffordances`) routes through here instead of re-implementing
33
- * the boolean/object-form parse locally. `base` is the bucket default an
34
- * omitted `enabled` falls back to (callers that only gate on an explicit
35
- * opt-out pass `true`; predicate extraction is independent of `base`).
40
+ * UI package (grid row affordances, related-list row predicates) routes through
41
+ * here instead of re-implementing the boolean/object-form parse locally. The
42
+ * spec keeps its equivalent (`normalizeRowCrudOverride`) module-private, so
43
+ * this stays a local declaration — under a name the spec does not own.
44
+ *
45
+ * `base` is the bucket default an omitted `enabled` falls back to (callers that
46
+ * only gate on an explicit opt-out pass `true`; predicate extraction is
47
+ * independent of `base`).
36
48
  */
37
49
  export function normalizeUserAction(v, base) {
38
50
  if (v == null)
@@ -59,24 +71,58 @@ export function normalizeUserAction(v, base) {
59
71
  export function userActionPredicates(v) {
60
72
  return normalizeUserAction(v, false).predicates;
61
73
  }
62
- /** Resolve the effective CRUD affordances for an object schema. */
63
- export function resolveCrudAffordances(obj) {
64
- const bucket = obj?.managedBy ?? 'platform';
65
- const base = DEFAULTS[bucket] ?? DEFAULTS.platform;
66
- const o = obj?.userActions ?? {};
67
- const edit = normalizeUserAction(o.edit, base.edit);
68
- const del = normalizeUserAction(o.delete, base.delete);
69
- const out = {
70
- create: o.create ?? base.create,
71
- import: o.import ?? base.import,
72
- edit: edit.enabled,
73
- delete: del.enabled,
74
- exportCsv: o.exportCsv ?? base.exportCsv,
75
- };
76
- if (edit.predicates)
77
- out.editPredicates = edit.predicates;
78
- if (del.predicates)
79
- out.deletePredicates = del.predicates;
74
+ /**
75
+ * The affordance-bit → server API-operation mapping used to intersect the
76
+ * UI-intent affordances with the server's effective API operation set (#3391).
77
+ * This is the objectui end of the framework's `API_METHOD_DERIVATION` contract:
78
+ * the button predicate is `affordance(op) ∧ effective.api(op)`.
79
+ */
80
+ const AFFORDANCE_TO_API_OPERATION = {
81
+ create: 'create',
82
+ import: 'import',
83
+ edit: 'update',
84
+ delete: 'delete',
85
+ exportCsv: 'export',
86
+ };
87
+ /**
88
+ * Resolve the CRUD affordances a UI surface should expose for an object —
89
+ * the spec's object-level matrix, INTERSECTED with what the server will
90
+ * actually accept.
91
+ *
92
+ * The bucket/`userActions` half is **not computed here**: it is delegated to
93
+ * `resolveCrudAffordances()` from `@objectstack/spec/data` (ADR-0103), so the
94
+ * bucket table has exactly one definition on the platform. Only the
95
+ * intersection below is objectui's, which is why this function no longer
96
+ * carries the spec's name (objectstack#4115).
97
+ *
98
+ * @param obj The object schema (managedBy bucket + userActions overrides).
99
+ * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
100
+ * set for this object (from `/me/permissions` `apiOperations`, #3391). When
101
+ * provided, each affordance bit is ANDed with the corresponding API operation
102
+ * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
103
+ * so the UI never shows a button the server would 405. Passing `undefined`
104
+ * (old backend / no effective set) leaves the affordances untouched —
105
+ * backward-compatible. An empty array means "expose nothing" → all bits off.
106
+ */
107
+ export function resolveEffectiveCrudAffordances(obj, effectiveApiOperations) {
108
+ // The spec's resolver is the single definition of the bucket defaults and the
109
+ // `userActions` override parse (including the #2614 object form). Its input
110
+ // type does not model the `null`s objectui's loosely-typed schemas carry, so
111
+ // they are normalised away at this one seam rather than re-deriving anything.
112
+ const out = resolveSpecCrudAffordances({
113
+ managedBy: obj?.managedBy ?? undefined,
114
+ userActions: (obj?.userActions ?? undefined),
115
+ });
116
+ // [#3391] Intersect with the server's effective API operations when present.
117
+ // The frontend consumes the effective set the server hands down; it never
118
+ // reads the raw `apiMethods` nor re-derives.
119
+ if (Array.isArray(effectiveApiOperations)) {
120
+ const eff = new Set(effectiveApiOperations);
121
+ for (const [bit, op] of Object.entries(AFFORDANCE_TO_API_OPERATION)) {
122
+ if (out[bit] && !eff.has(op))
123
+ out[bit] = false;
124
+ }
125
+ }
80
126
  return out;
81
127
  }
82
128
  /** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
@@ -84,15 +130,26 @@ export function isWriteOptedIn(v) {
84
130
  return v === true || (typeof v === 'object' && v !== null && v.enabled === true);
85
131
  }
86
132
  /**
87
- * ADR-0103 — a `system`-bucket object that opened ANY write via `userActions`
88
- * is admin/user-writable DATA, not an engine-owned monitoring surface. Used to
89
- * pick the writable-system badge/empty-state copy.
133
+ * True for a platform-defined schema whose DATA is admin/user-writable — the
134
+ * `system-data` bucket. Used to pick the writable-system badge/empty-state copy
135
+ * instead of the engine-owned "read-only monitoring surface" copy.
136
+ *
137
+ * **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split
138
+ * this had to RECOVER the distinction from `userActions`: `system` doubled as
139
+ * both the engine-owned default and the writable set, so "did it open any
140
+ * write?" was the only way to tell a Notification Preferences grid from an
141
+ * automation-run log. v17 retired that overload — the writable half is now the
142
+ * `system-data` bucket (writable by DEFAULT) and the engine-owned half is
143
+ * `engine-owned` — so the bucket answers the question directly and the
144
+ * `userActions` probe would only re-derive what the value already states.
145
+ *
146
+ * The narrowing case is deliberately still `true`: a `system-data` grid that
147
+ * narrows to edit-only is still platform-schema/user-data and should read as
148
+ * such. The spec refuses `system-data` on an object that narrows away every
149
+ * write, so there is no "writable bucket with no writes" shape to worry about.
90
150
  */
91
151
  export function isSystemWritable(obj) {
92
- if (obj?.managedBy !== 'system')
93
- return false;
94
- const o = obj?.userActions ?? {};
95
- return o.create === true || isWriteOptedIn(o.edit) || isWriteOptedIn(o.delete);
152
+ return obj?.managedBy === 'system-data';
96
153
  }
97
154
  /**
98
155
  * Whether an object's rows may be edited in place (detail inline-edit / form
@@ -100,7 +157,14 @@ export function isSystemWritable(obj) {
100
157
  * `append-only` / `better-auth` objects resolve to `false` unless they opened
101
158
  * `userActions.edit`. (Per-record predicates gate row action buttons, not
102
159
  * object-level editability, so only the resolved boolean matters here.)
160
+ *
161
+ * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
162
+ * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
163
+ * provided, inline-edit is additionally ANDed with the server allowing
164
+ * `update`, so a record page never offers double-click/pencil editing the
165
+ * server would 405. `undefined` (old backend / unrestricted) leaves the
166
+ * bucket affordance untouched.
103
167
  */
104
- export function isObjectInlineEditable(obj) {
105
- return resolveCrudAffordances(obj).edit;
168
+ export function isObjectInlineEditable(obj, effectiveApiOperations) {
169
+ return resolveEffectiveCrudAffordances(obj, effectiveApiOperations).edit;
106
170
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ObjectUI — list-view vocabulary canonicalization
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 { RowHeight } from '@objectstack/spec/ui';
9
+ /** ListView's toolbar density vocabulary — three steps, not the spec's five. */
10
+ export type DensityMode = 'compact' | 'comfortable' | 'spacious';
11
+ /**
12
+ * The spec's `RowHeightSchema` vocabulary — **re-exported, not re-declared**.
13
+ *
14
+ * The hand-written union that used to sit here already advertised itself as
15
+ * "the spec's `RowHeightSchema` vocabulary" in its doc comment, which is the
16
+ * failure class objectstack#4115 is about: the claim was true when written and
17
+ * nothing would have caught the day it stopped being true. The maps below are
18
+ * `Record<RowHeight, …>`, so a spec-side addition now fails the build here
19
+ * instead of silently leaving a row height with no density mapping.
20
+ */
21
+ export type { RowHeight };
22
+ /**
23
+ * `densityMode` → `rowHeight`. The two vocabularies are not the same size: the
24
+ * spec has five row heights, ListView's toolbar offers three. Widening maps
25
+ * each density onto the spec value the renderer already resolves it back to
26
+ * (see {@link ROW_HEIGHT_TO_DENSITY_MODE}), so a fold followed by a read is a
27
+ * round trip — `spacious` → `tall` → `spacious`.
28
+ */
29
+ export declare const DENSITY_MODE_TO_ROW_HEIGHT: Record<DensityMode, RowHeight>;
30
+ /**
31
+ * `rowHeight` → `densityMode`, the read direction. Narrowing five values onto
32
+ * three is lossy by construction: `short` collapses into `compact` and
33
+ * `extra_tall` into `spacious`. Exported so the renderer and the persistence
34
+ * layer agree on the collapse instead of each hard-coding its own table.
35
+ */
36
+ export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
37
+ /**
38
+ * Tolerant reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}. View metadata is
39
+ * user-authored, so `rowHeight` is not guaranteed to be one of the five spec
40
+ * values — an unknown one lands on `comfortable` rather than rendering an
41
+ * undefined density. Keeping the fallback here means every surface collapses
42
+ * the same way.
43
+ */
44
+ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode;
45
+ /**
46
+ * ObjectUI's `list-view` node historically used a different vocabulary from
47
+ * `@objectstack/spec` for the same concepts (`fields` where the spec says
48
+ * `columns`, `viewType` where it says `type`, …). Issue #2231 closed the
49
+ * type-level fork; #2890 closes the vocabulary fork.
50
+ *
51
+ * Stored view metadata in user databases still carries the legacy keys, so the
52
+ * renderer cannot simply stop accepting them. Per AGENTS.md Commandment #0.1 the
53
+ * answer is NOT a per-read-site `??` fallback — those fossilize a second de-facto
54
+ * contract and drift apart (they already had: `ObjectGrid` preferred `columns`
55
+ * in one branch and `fields` in another). Instead legacy acceptance lives HERE,
56
+ * in one documented normalizer at the component boundary, mirroring
57
+ * `normalizeSchemaReferenceKeys` (object schemas) and the spec's own
58
+ * `normalizeVisibleWhen` / `normalizeFilterOperator` migration bridges.
59
+ *
60
+ * Note this cannot be a `z.preprocess` on `ListViewSchema`: nothing on the
61
+ * render path parses view metadata through zod (the zod schemas are used by the
62
+ * CLI validator, the VS Code extension and tests only), so a schema-level fold
63
+ * would never run. The guarantee comes from the call site instead — `ListView`
64
+ * normalizes before it reads anything.
65
+ *
66
+ * The fold is deliberately one-directional: the canonical key wins when both are
67
+ * present, and the legacy key is REMOVED from the result so a read-site that was
68
+ * missed fails loudly instead of quietly taking the legacy path. Like a spec
69
+ * migration bridge, this is expected to be dropped in a future major once stored
70
+ * metadata has been migrated.
71
+ *
72
+ * Non-mutating and allocation-frugal: returns the input by reference when there
73
+ * is nothing to fold, so `ListView`'s downstream `useMemo`s keep a stable
74
+ * dependency identity on the common (already-canonical) path.
75
+ *
76
+ * Currently folded:
77
+ * - `fields` → `columns` (#2890 scope A step 1)
78
+ * - `densityMode` → `rowHeight` (#2890 scope A step 2)
79
+ * - the `show*` toolbar flags → `userActions` (#2890 scope A step 3), and
80
+ * `showDescription` → `appearance.showDescription`. The canonical key wins
81
+ * per-flag, so a view may carry `userActions` for some toggles and a legacy
82
+ * flag for others. NOTE the fold does not apply any default: an absent flag
83
+ * stays absent, because the defaults are per-toggle (search/sort/filter/
84
+ * rowHeight/group default ON, hideFields/rowColor default OFF) and belong to
85
+ * the renderer, not to the vocabulary bridge.
86
+ * - `aria: { label, describedBy }` → the spec's `AriaProps`
87
+ * (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }`
88
+ * → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live`
89
+ * survives untouched: it has no spec counterpart.
90
+ * - `filters` → `filter` (#2890 scope A step 4). A key rename only: BOTH keys
91
+ * carry an ObjectQL FilterNode array (`[['stage','=','won']]`) everywhere in
92
+ * objectui — every consumer passes the value straight to `$filter`. The spec
93
+ * types `filter` as `ViewFilterRule[]` (`{field, operator, value}` objects),
94
+ * so objectui's field is typed from the spec but used as something else.
95
+ * That mismatch is real and out of scope here; converting formats inside a
96
+ * vocabulary fold would change what reaches the data source.
97
+ * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
98
+ * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
99
+ * — otherwise it reaches the renderer's typeless default branch and shows as
100
+ * a red "Unknown component type" box.
101
+ * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
102
+ * (#3104). This one MIRRORS instead of deleting, for the reason given on
103
+ * {@link normalizeColumnIdentities}: `columns` entries cross the package
104
+ * boundary into host renderers, so dropping `name` from under them is a
105
+ * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
106
+ * above, so a view that spells its column list the legacy way still gets its
107
+ * entries canonicalized.
108
+ */
109
+ export declare function normalizeListViewSchema<T>(schema: T): T;