@object-ui/core 17.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.
@@ -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)
@@ -73,7 +85,15 @@ const AFFORDANCE_TO_API_OPERATION = {
73
85
  exportCsv: 'export',
74
86
  };
75
87
  /**
76
- * Resolve the effective CRUD affordances for an object schema.
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).
77
97
  *
78
98
  * @param obj The object schema (managedBy bucket + userActions overrides).
79
99
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
@@ -84,23 +104,15 @@ const AFFORDANCE_TO_API_OPERATION = {
84
104
  * (old backend / no effective set) leaves the affordances untouched —
85
105
  * backward-compatible. An empty array means "expose nothing" → all bits off.
86
106
  */
87
- export function resolveCrudAffordances(obj, effectiveApiOperations) {
88
- const bucket = obj?.managedBy ?? 'platform';
89
- const base = DEFAULTS[bucket] ?? DEFAULTS.platform;
90
- const o = obj?.userActions ?? {};
91
- const edit = normalizeUserAction(o.edit, base.edit);
92
- const del = normalizeUserAction(o.delete, base.delete);
93
- const out = {
94
- create: o.create ?? base.create,
95
- import: o.import ?? base.import,
96
- edit: edit.enabled,
97
- delete: del.enabled,
98
- exportCsv: o.exportCsv ?? base.exportCsv,
99
- };
100
- if (edit.predicates)
101
- out.editPredicates = edit.predicates;
102
- if (del.predicates)
103
- out.deletePredicates = del.predicates;
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
+ });
104
116
  // [#3391] Intersect with the server's effective API operations when present.
105
117
  // The frontend consumes the effective set the server hands down; it never
106
118
  // reads the raw `apiMethods` nor re-derives.
@@ -118,15 +130,26 @@ export function isWriteOptedIn(v) {
118
130
  return v === true || (typeof v === 'object' && v !== null && v.enabled === true);
119
131
  }
120
132
  /**
121
- * ADR-0103 — a `system`-bucket object that opened ANY write via `userActions`
122
- * is admin/user-writable DATA, not an engine-owned monitoring surface. Used to
123
- * 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.
124
150
  */
125
151
  export function isSystemWritable(obj) {
126
- if (obj?.managedBy !== 'system')
127
- return false;
128
- const o = obj?.userActions ?? {};
129
- return o.create === true || isWriteOptedIn(o.edit) || isWriteOptedIn(o.delete);
152
+ return obj?.managedBy === 'system-data';
130
153
  }
131
154
  /**
132
155
  * Whether an object's rows may be edited in place (detail inline-edit / form
@@ -143,5 +166,5 @@ export function isSystemWritable(obj) {
143
166
  * bucket affordance untouched.
144
167
  */
145
168
  export function isObjectInlineEditable(obj, effectiveApiOperations) {
146
- return resolveCrudAffordances(obj, effectiveApiOperations).edit;
169
+ return resolveEffectiveCrudAffordances(obj, effectiveApiOperations).edit;
147
170
  }
@@ -5,10 +5,20 @@
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 { RowHeight } from '@objectstack/spec/ui';
8
9
  /** ListView's toolbar density vocabulary — three steps, not the spec's five. */
9
10
  export type DensityMode = 'compact' | 'comfortable' | 'spacious';
10
- /** The spec's `RowHeightSchema` vocabulary. */
11
- export type RowHeight = 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall';
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 };
12
22
  /**
13
23
  * `densityMode` → `rowHeight`. The two vocabularies are not the same size: the
14
24
  * spec has five row heights, ListView's toolbar offers three. Widening maps
@@ -12,6 +12,7 @@
12
12
  * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
13
  * is client-side form validation, not a mirror of a server rule.
14
14
  */
15
+ export * from './required-presence.js';
15
16
  export * from './validation-engine.js';
16
17
  export * from './schema-validator.js';
17
18
  export * from './validators/index.js';
@@ -12,6 +12,7 @@
12
12
  * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
13
  * is client-side form validation, not a mirror of a server rule.
14
14
  */
15
+ export * from './required-presence.js';
15
16
  export * from './validation-engine.js';
16
17
  export * from './schema-validator.js';
17
18
  export * from './validators/index.js';
@@ -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
+ * The ONE client-side answer to "is this field's value absent?" for a
10
+ * `required` check.
11
+ *
12
+ * `required` is a PRESENCE contract, not a truthiness one. `@objectstack/spec`
13
+ * FieldSchema.required (ADR-0113) reads "an insert must provide a NON-NULL
14
+ * value", and objectql's record validator implements exactly that — its
15
+ * `isMissing` is `undefined | null | blank string`, nothing more. This mirrors
16
+ * that verdict so the form, the wizard's cross-step gate, and the server all
17
+ * agree instead of each inventing a dialect.
18
+ *
19
+ * What this deliberately does NOT treat as absent:
20
+ * - `false` — a boolean's second value. react-hook-form's own `required` rule
21
+ * fails on `isBoolean(v) && !v`, which quietly turns a required checkbox into
22
+ * "must be TRUE": an AI-built task tracker could not create an UNFINISHED
23
+ * task, because the state the switch shows by default was the one value it
24
+ * refused to save (cloud#972).
25
+ * - `0` — a number's identity element. "Zero hours logged" is data.
26
+ *
27
+ * An empty ARRAY *is* absent: the server has no array-valued scalar to
28
+ * disagree about, and "nothing picked" is a multiselect's own empty state,
29
+ * exactly as a blank string is a text input's.
30
+ */
31
+ export declare function isMissingForRequired(value: unknown): boolean;
@@ -0,0 +1,36 @@
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
+ * The ONE client-side answer to "is this field's value absent?" for a
10
+ * `required` check.
11
+ *
12
+ * `required` is a PRESENCE contract, not a truthiness one. `@objectstack/spec`
13
+ * FieldSchema.required (ADR-0113) reads "an insert must provide a NON-NULL
14
+ * value", and objectql's record validator implements exactly that — its
15
+ * `isMissing` is `undefined | null | blank string`, nothing more. This mirrors
16
+ * that verdict so the form, the wizard's cross-step gate, and the server all
17
+ * agree instead of each inventing a dialect.
18
+ *
19
+ * What this deliberately does NOT treat as absent:
20
+ * - `false` — a boolean's second value. react-hook-form's own `required` rule
21
+ * fails on `isBoolean(v) && !v`, which quietly turns a required checkbox into
22
+ * "must be TRUE": an AI-built task tracker could not create an UNFINISHED
23
+ * task, because the state the switch shows by default was the one value it
24
+ * refused to save (cloud#972).
25
+ * - `0` — a number's identity element. "Zero hours logged" is data.
26
+ *
27
+ * An empty ARRAY *is* absent: the server has no array-valued scalar to
28
+ * disagree about, and "nothing picked" is a multiselect's own empty state,
29
+ * exactly as a blank string is a text input's.
30
+ */
31
+ export function isMissingForRequired(value) {
32
+ return (value === undefined ||
33
+ value === null ||
34
+ (typeof value === 'string' && value.trim() === '') ||
35
+ (Array.isArray(value) && value.length === 0));
36
+ }
@@ -16,21 +16,37 @@
16
16
  */
17
17
  import type { BaseSchema } from '@object-ui/types';
18
18
  /**
19
- * Validation error details
19
+ * One issue found while walking an ObjectUI schema TREE — `path` locates the
20
+ * offending node (e.g. `children[0].type`), and `type` says whether it is fatal.
21
+ *
22
+ * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
23
+ * interface carried until objectstack#4115. That one describes PLUGIN-manifest
24
+ * validation and is keyed by `field`, with no severity discriminant. The two
25
+ * share no key beyond `message`/`code`.
26
+ *
27
+ * Naming convention for this family, set here and followed by the rest of the
28
+ * burn-down: `<what-is-validated>Validation<Error|Result>`. (`SchemaValidationResult`
29
+ * and `SchemaValidationReport` are themselves spec exports — checked, not assumed.)
20
30
  */
21
- export interface ValidationError {
31
+ export interface SchemaNodeValidationError {
22
32
  path: string;
23
33
  message: string;
24
34
  type: 'error' | 'warning';
25
35
  code?: string;
26
36
  }
27
37
  /**
28
- * Validation result
38
+ * The outcome of validating an ObjectUI schema tree.
39
+ *
40
+ * NOT the spec's `ValidationResult` (`@objectstack/spec/kernel` and
41
+ * `@objectstack/spec/contracts`), whose name this interface carried until
42
+ * objectstack#4115: there `errors`/`warnings` are OPTIONAL arrays of
43
+ * `{ field, message, code? }` describing a plugin manifest. Here both arrays are
44
+ * required and carry node paths.
29
45
  */
30
- export interface ValidationResult {
46
+ export interface SchemaNodeValidationResult {
31
47
  valid: boolean;
32
- errors: ValidationError[];
33
- warnings: ValidationError[];
48
+ errors: SchemaNodeValidationError[];
49
+ warnings: SchemaNodeValidationError[];
34
50
  }
35
51
  /**
36
52
  * Validate a complete schema
@@ -53,7 +69,7 @@ export interface ValidationResult {
53
69
  * }
54
70
  * ```
55
71
  */
56
- export declare function validateSchema(schema: any, path?: string): ValidationResult;
72
+ export declare function validateSchema(schema: any, path?: string): SchemaNodeValidationResult;
57
73
  /**
58
74
  * Assert that a schema is valid, throwing an error if not
59
75
  *
@@ -91,4 +107,4 @@ export declare function isValidSchema(value: any): value is BaseSchema;
91
107
  * @param result - The validation result
92
108
  * @returns Formatted error summary
93
109
  */
94
- export declare function formatValidationErrors(result: ValidationResult): string;
110
+ export declare function formatValidationErrors(result: SchemaNodeValidationResult): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/core",
3
- "version": "17.1.0",
3
+ "version": "17.2.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": "^17.0.0-rc.0",
29
- "@objectstack/spec": "^17.0.0-rc.0",
28
+ "@objectstack/formula": "^17.0.0-rc.1",
29
+ "@objectstack/spec": "^17.0.0-rc.1",
30
30
  "lodash": "^4.18.1",
31
31
  "zod": "^4.4.3",
32
- "@object-ui/types": "17.1.0"
32
+ "@object-ui/types": "17.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^6.0.3",