@object-ui/core 15.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/dist/actions/ActionRunner.d.ts +6 -2
- package/dist/adapters/ApiDataSource.d.ts +11 -1
- package/dist/adapters/ApiDataSource.js +11 -0
- package/dist/adapters/ValueDataSource.d.ts +9 -1
- package/dist/adapters/ValueDataSource.js +9 -0
- package/dist/adapters/batchTransaction.d.ts +49 -0
- package/dist/adapters/batchTransaction.js +118 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +10 -0
- package/dist/evaluator/ExpressionEvaluator.js +48 -0
- package/dist/evaluator/SafeExpressionParser.js +5 -2
- package/dist/evaluator/optionRules.d.ts +22 -0
- package/dist/evaluator/optionRules.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +14 -1
- package/dist/utils/export-filename.js +4 -1
- package/dist/utils/managedBy.d.ts +101 -0
- package/dist/utils/managedBy.js +106 -0
- package/dist/utils/record-title.js +0 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,252 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 16.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1c8935a: feat(app-shell): render ActionParamDialog params through the shared form field-widget renderer (ADR-0059, #2700)
|
|
8
|
+
|
|
9
|
+
`ActionParamDialog` no longer hand-rolls a per-type ternary chain (select /
|
|
10
|
+
lookup / textarea / number / boolean, everything else → text input). Every
|
|
11
|
+
declared action param now renders through the same `fieldWidgetMap` the object
|
|
12
|
+
form uses, so a param of ANY form-supported field type — `file`, `image`,
|
|
13
|
+
`richtext`, `markdown`, `color`, `address`, `code`, `date`, … — gets its real
|
|
14
|
+
widget, lazily loaded behind `Suspense`. Subsumes the single `file` branch ask
|
|
15
|
+
in #2698: `type: 'file'` params render the real `FileField` upload control via
|
|
16
|
+
the ambient `UploadProvider`, honoring `multiple`/`accept`/`maxSize`.
|
|
17
|
+
|
|
18
|
+
- `@object-ui/fields`: new exports `resolveFormWidgetType(type)` (widget-key
|
|
19
|
+
resolution incl. spec aliases, text fallback) and `getLazyFieldWidget(type)`
|
|
20
|
+
(per-type-cached `React.lazy` over the form's own widget loaders).
|
|
21
|
+
- `@object-ui/core`: `ActionParamDef` gains `accept`/`maxSize`; `multiple` is
|
|
22
|
+
now general widget config (was lookup-only).
|
|
23
|
+
- `@object-ui/app-shell`: new pure `paramToField()` adapter (param → field
|
|
24
|
+
shape) with a drift test pinning param support ⊇ form support (`FORM_FIELD_TYPES`),
|
|
25
|
+
mirroring the FieldEditWidget parity guard; `resolveActionParams()` inherits
|
|
26
|
+
`multiple`/`accept`/`maxSize` from the referenced field for every type.
|
|
27
|
+
`required` validation, `visible` CEL gating, helpText, error styling, and
|
|
28
|
+
value shapes for previously-supported types are unchanged.
|
|
29
|
+
|
|
30
|
+
- 2e7d7f0: feat(evaluator): route `{ dialect: 'cel' }` component/action predicates to the canonical CEL engine (#2661)
|
|
31
|
+
|
|
32
|
+
Component and action `visible` / `disabled` / `hidden` predicates were evaluated
|
|
33
|
+
by the home-grown JS `ExpressionEvaluator`, while field rules
|
|
34
|
+
(`visibleWhen`/`readonlyWhen`/`requiredWhen`, via `fieldRules.ts`) and row/list
|
|
35
|
+
conditionals (via `evalRowPredicate`) already delegate to the canonical
|
|
36
|
+
`@objectstack/formula` engine. That split meant a `{ dialect: 'cel' }` predicate
|
|
37
|
+
in a renderer/action surface was executed as **JavaScript** — CEL-only forms
|
|
38
|
+
(`x in list`, `has()`, typed `==`, the `today()`/`daysFromNow()` catalog) behaved
|
|
39
|
+
differently from, or faulted against, the server's enforcement.
|
|
40
|
+
|
|
41
|
+
This converges the remaining tier onto the same engine:
|
|
42
|
+
|
|
43
|
+
- **`@object-ui/core`** — `ExpressionEvaluator.evaluateCondition` now detects a
|
|
44
|
+
`{ dialect: 'cel', source }` envelope and evaluates it on `@objectstack/formula`
|
|
45
|
+
(via `evalFieldPredicate`), binding the `record` namespace plus the whole
|
|
46
|
+
context bag as top-level scope (`record.*`, `features.*`, `user.*`, `app.*`).
|
|
47
|
+
Fail-soft to visible/enabled to match the legacy default; `throwOnError`
|
|
48
|
+
callers still fail closed on a _faulting_ predicate (a genuine `false` never
|
|
49
|
+
throws). This fixes every `SchemaRenderer` visibility/disabled read at once.
|
|
50
|
+
- **`@object-ui/react`** — `toPredicateInput` preserves a CEL envelope instead of
|
|
51
|
+
collapsing it to a `${source}` string, and `useCondition` accepts and forwards
|
|
52
|
+
the envelope (keyed on a stable `(dialect, source)` so it doesn't re-evaluate
|
|
53
|
+
each render). Action buttons (`action-icon`/`group`/`bar`/`button`) therefore
|
|
54
|
+
evaluate CEL `visible`/`enabled`/`disabled` on the canonical engine.
|
|
55
|
+
|
|
56
|
+
**Back-compat:** bare strings and `${…}` templates stay on the legacy JS path
|
|
57
|
+
(deprecation window); only an explicit `{ dialect: 'cel' }` envelope is rerouted.
|
|
58
|
+
`{ dialect: 'template' }` is unaffected.
|
|
59
|
+
|
|
60
|
+
Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
|
|
61
|
+
reaches the identical verdict as the server — including the framework's
|
|
62
|
+
`dateField == today()` equality fix (objectstack-ai/framework#3205) once it
|
|
63
|
+
lands in a published 15.x. The broader home-grown-vs-canonical divergence
|
|
64
|
+
motivation is #2661.
|
|
65
|
+
|
|
66
|
+
- 31b77d4: **Add the explicit `engine-owned` lifecycle bucket (tracks framework ADR-0103 addendum / #3343).** The framework split the overloaded `managedBy: 'system'` bucket by promoting the engine-owned case to its own enum value; this mirrors it in the UI type + runtime + badge.
|
|
67
|
+
|
|
68
|
+
- **`@object-ui/types`** — `ManagedByBucket` union and `MANAGED_BY_BUCKETS` gain `'engine-owned'` (canonical order: `platform, config, system, engine-owned, append-only, better-auth`). The union stays closed, so every consumer that missed the new value is a compile error.
|
|
69
|
+
- **`@object-ui/core`** — `resolveCrudAffordances` gains the `engine-owned` default row (identical all-locked matrix as `system`/`append-only`), so `isObjectInlineEditable` / the grid + form gates treat it as read-only automatically.
|
|
70
|
+
- **`@object-ui/app-shell`** — the `ManagedByBadge` renders `engine-owned` with the same read-only "System-managed" copy as a locked `system` object (reuses the existing `managedByBadge.system` i18n key — zero translation churn; the distinction is at the schema level, not the user-facing string), and `resolveManagedByEmptyState` reuses the `system` engine-owned empty state.
|
|
71
|
+
|
|
72
|
+
Behaviour-preserving: `engine-owned` resolves to the same locked affordances `system` did by default, so nothing about how a locked object renders changes — the value just makes the schema self-documenting. New unit coverage for the bucket in `resolveCrudAffordances` / `isObjectInlineEditable` / `MANAGED_BY_BUCKETS` / the empty-state helper.
|
|
73
|
+
|
|
74
|
+
- 62b9ab5: feat(data): unify master-detail saves behind `DataSource.batchTransaction`, isolate the non-atomic fallback in the adapter (#2679)
|
|
75
|
+
|
|
76
|
+
Master-detail saves (`MasterDetailForm`, `LineItemsPanel`) now always persist
|
|
77
|
+
through `dataSource.batchTransaction(operations)` — one ordered cross-object
|
|
78
|
+
operation list, with `{ $ref: <op index> }` linking a child to a parent created
|
|
79
|
+
in the same batch. The form no longer contains any client-side orchestration or
|
|
80
|
+
best-effort compensation-delete; that atomicity anti-pattern is gone from the UI
|
|
81
|
+
layer (framework #1604 / framework ADR-0034 item 4).
|
|
82
|
+
|
|
83
|
+
- **`@object-ui/types`** — `batchTransaction?` is now a first-class (optional)
|
|
84
|
+
method on the `DataSource` contract, typed via `BatchTransactionOperation` /
|
|
85
|
+
`BatchRef`. Replaces the previous `(dataSource as any).batchTransaction`
|
|
86
|
+
method-sniffing.
|
|
87
|
+
- **`@object-ui/core`** — new `emulateBatchTransaction(dataSource, operations)`
|
|
88
|
+
(sequential writes, `$ref` resolution, best-effort reverse-order compensation)
|
|
89
|
+
and `runBatchTransaction(dataSource, operations)` (prefers the adapter's method,
|
|
90
|
+
emulates otherwise). `ApiDataSource` / `ValueDataSource` implement
|
|
91
|
+
`batchTransaction` via the emulation.
|
|
92
|
+
- **`@object-ui/data-objectstack`** — `ObjectStackAdapter.batchTransaction` uses
|
|
93
|
+
the server's atomic `POST /api/v1/batch`, prefers the typed
|
|
94
|
+
`client.data.batchTransaction` SDK method when the installed client exposes it,
|
|
95
|
+
and degrades to the client-side emulation ONLY when the endpoint is missing
|
|
96
|
+
(404/405) or the runtime can't do transactions (501). Real errors (400/401/403/
|
|
97
|
+
409/500) still surface. This is the isolated, tested home of the non-atomic
|
|
98
|
+
fallback.
|
|
99
|
+
- **`@object-ui/plugin-form`** — removed `applyDetail` / `createMany` /
|
|
100
|
+
`ApplyDetailResult` from `masterDetailTx.ts`; `MasterDetailForm` and
|
|
101
|
+
`LineItemsPanel` build ops and call `runBatchTransaction`. `LineItemsPanel`
|
|
102
|
+
saves are now atomic on a capable backend, with the rollup folded into the same
|
|
103
|
+
batch.
|
|
104
|
+
|
|
105
|
+
No behavior change on a current ObjectStack backend (it has `/api/v1/batch`);
|
|
106
|
+
older/limited backends keep a working — now clearly non-atomic — save path.
|
|
107
|
+
|
|
108
|
+
- 1629313: feat(fields): RadioField per-option `visibleWhen` cascading + `dependsOn` gating; single-source the option resolver
|
|
109
|
+
|
|
110
|
+
Brings `RadioField` to parity with `SelectField` / `MultiSelectField` for ADR-0058
|
|
111
|
+
cascading & role-gated options, and collapses the three copies of the
|
|
112
|
+
gate-then-filter logic onto one shared resolver.
|
|
113
|
+
|
|
114
|
+
- **`@object-ui/core`**: new pure `resolveCascadingOptions(rawOptions, record, dependsOn, scope)`
|
|
115
|
+
→ `{ options, gated, dependsOnFields }` — the single source of truth for
|
|
116
|
+
`dependsOn` gating + per-option `visibleWhen` filtering.
|
|
117
|
+
- **`@object-ui/fields`**: `RadioField` now narrows its offered radios against
|
|
118
|
+
the live record + `current_user`, gates behind a "select the parent first"
|
|
119
|
+
hint while a `dependsOn` field is empty, and clears a value no longer offered
|
|
120
|
+
(scalar cascade clear). The `useCascadingOptions` hook is refactored to a thin
|
|
121
|
+
React wrapper over `resolveCascadingOptions`.
|
|
122
|
+
- **`@object-ui/components`**: the form renderer's inline option pre-filter and
|
|
123
|
+
cross-field cascade-clear effect now call `resolveCascadingOptions` instead of
|
|
124
|
+
re-deriving gating/filtering, so they can't drift from the widgets (no
|
|
125
|
+
behavior change).
|
|
126
|
+
- Tests: `RadioField.cascade.test.tsx` mirrors the select cascade tests; core
|
|
127
|
+
gains `resolveCascadingOptions` unit coverage.
|
|
128
|
+
|
|
129
|
+
- 2331ac9: feat(report): drill a date-bucket cell into its time range, not a superset (#1752)
|
|
130
|
+
|
|
131
|
+
Clicking a report/dashboard cell grouped by a `dateGranularity` date dimension
|
|
132
|
+
("2026-Q2") used to drill into a **superset** — the date dimension was skipped,
|
|
133
|
+
so the record list spanned every time bucket. It now scopes to the clicked
|
|
134
|
+
bucket's half-open range, consuming the framework's new `drillRanges` sidecar.
|
|
135
|
+
|
|
136
|
+
- **`@object-ui/core`** — `buildDatasetDrillFilter` accepts the per-row
|
|
137
|
+
`drillRanges` and emits an ObjectQL range operator object
|
|
138
|
+
(`{ [field]: { $gte, $lt } }`) alongside the equality dims.
|
|
139
|
+
- **`@object-ui/plugin-report` / `@object-ui/plugin-dashboard`** — the report
|
|
140
|
+
renderer and dashboard widget forward `drillRanges`, and a **date-only**
|
|
141
|
+
report (no equality drill dim) is now drillable via the range alone.
|
|
142
|
+
- **`@object-ui/app-shell`** — the "Open in list →" escape hatch
|
|
143
|
+
(`useOpenRecordList`) now targets the ADR-0055 **bare data surface**
|
|
144
|
+
(`/:object/data`, "the URL is the view" — no baked-in view filter to
|
|
145
|
+
over-narrow the drill) and serializes a range to the
|
|
146
|
+
`filter[field][gte|lt]` operator contract. `ObjectDataPage` parses those
|
|
147
|
+
operators (equality shorthand unchanged), renders a range as a single chip,
|
|
148
|
+
and removes both bounds together. A new `drillUrlFilters` module owns the
|
|
149
|
+
write/read serialization so both sides can't drift (round-trip tested).
|
|
150
|
+
|
|
151
|
+
Companion to the framework analytics change (objectstack-ai/framework#3256).
|
|
152
|
+
|
|
153
|
+
### Patch Changes
|
|
154
|
+
|
|
155
|
+
- 8b8b744: chore(deps): align `@objectstack/formula` / `lint` / `client` to `^15.1.1`
|
|
156
|
+
|
|
157
|
+
These three were still pinned to `^14.6.0` while `@objectstack/spec` was already
|
|
158
|
+
`^15.1.1` — a version skew from the v15 upgrade (formula/lint/client publish in
|
|
159
|
+
lockstep with spec, and their own 15.0.0 entries are pure dependency bumps, so
|
|
160
|
+
this is alignment, not a behavioral migration).
|
|
161
|
+
|
|
162
|
+
Practical effect: the client-side field-rule evaluation
|
|
163
|
+
(`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
|
|
164
|
+
to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
|
|
165
|
+
will pick up the framework's `dateField == today()` equality fix
|
|
166
|
+
(objectstack-ai/framework#3205) automatically at the next 15.x release via the
|
|
167
|
+
caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
|
|
168
|
+
use the home-grown JS evaluator — tracked separately in #2661).
|
|
169
|
+
|
|
170
|
+
- 7cf4051: chore(deps): align every `@objectstack/*` dependency to `^16.0.0-rc.0`
|
|
171
|
+
|
|
172
|
+
Bumps `@objectstack/spec` / `client` / `formula` / `lint` from `^15.1.1` to the
|
|
173
|
+
`16.0.0-rc.0` pre-release across the workspace (root + `apps/console` +
|
|
174
|
+
`apps/site` + all consuming packages). ObjectUI's own packages are already on
|
|
175
|
+
major 16, so this closes the 15↔16 skew between ObjectUI and the `@objectstack`
|
|
176
|
+
contract libraries (which publish in lockstep with `spec`).
|
|
177
|
+
|
|
178
|
+
This is a dependency alignment, not a behavioral migration: the full workspace
|
|
179
|
+
build (43/43) and the `@objectstack`-consuming package test suites
|
|
180
|
+
(`core` / `app-shell` / `data-objectstack` / `plugin-form` / `types`) are green
|
|
181
|
+
against `16.0.0-rc.0` with no source changes required.
|
|
182
|
+
|
|
183
|
+
Practical effect: `@objectstack/client@16.0.0-rc.0` now ships
|
|
184
|
+
`data.batchTransaction` (framework #3271), so `ObjectStackAdapter`'s feature
|
|
185
|
+
detect (`typeof client.data.batchTransaction === 'function'`) routes
|
|
186
|
+
master-detail cross-object saves through the typed SDK method instead of the
|
|
187
|
+
raw `fetch('/api/v1/batch')` fallback — realizing the "verify SDK path" half of
|
|
188
|
+
#2694. The raw-fetch branch stays as a defensive fallback (removal tracked in
|
|
189
|
+
#2694).
|
|
190
|
+
|
|
191
|
+
- 6d4fbe6: **Consolidate the `managedBy` lifecycle-bucket logic into one shared source of truth (follows framework ADR-0103).** The bucket taxonomy was hand-mirrored in several places — `crudAffordances.ts`, `ManagedByBadge.tsx` (its own `Bucket` union + `isWriteOptedIn` + the writable-system derivation), and `plugin-detail`'s `record-details.tsx` (`NON_EDITABLE_BUCKETS`, duplicated because it can't depend on app-shell) — a drift risk, and the object-schema `managedBy` type was open-ended (`(string & {})`) so unknown buckets slipped through and silently defaulted to fully-editable.
|
|
192
|
+
|
|
193
|
+
- **`@object-ui/types`** now owns the closed `ManagedByBucket` union (+ `MANAGED_BY_BUCKETS`), and `ObjectSchema.managedBy` is tightened from `'platform' | 'better-auth' | (string & {})` to that union — unknown buckets are now a type error at authoring time.
|
|
194
|
+
- **`@object-ui/core`** now owns the React-free runtime logic — `resolveCrudAffordances`, `isWriteOptedIn`, `isSystemWritable`, `isObjectInlineEditable` — reachable by every UI package including `plugin-detail` (which could not import app-shell).
|
|
195
|
+
- **`app-shell/utils/crudAffordances.ts`** is now a thin re-export of `@object-ui/core` (existing imports keep working); `ManagedByBadge` consumes the shared `isSystemWritable`; `plugin-detail` `record-details.tsx` replaces its hand-mirrored `NON_EDITABLE_BUCKETS` with `isObjectInlineEditable`.
|
|
196
|
+
|
|
197
|
+
Behavior-preserving — all existing affordance/edit-gate tests stay green; the shared module adds direct unit coverage (including the previously-untested `isSystemWritable` derivation). Translated copy (badge variants, empty-state messages) stays in app-shell.
|
|
198
|
+
|
|
199
|
+
- 0a3710b: **Finish the `managedBy` / `userActions` de-dup — one parser for the override shape (completes objectui#2712, framework#3343).** #2712 consolidated the bucket _union_ + affordance _set_ mirrors but left four surfaces still parsing the `userActions.{create,edit,delete}` override shape by hand. They now all route through the shared `@object-ui/core` policy, so no package re-implements the boolean / #2614-object-form parse locally.
|
|
200
|
+
|
|
201
|
+
- **`@object-ui/core`** promotes the internal `normalizeOverride` to the exported **`normalizeUserAction(v, base)`** (the one parser) and adds **`userActionPredicates(v)`** for per-record CEL predicate extraction.
|
|
202
|
+
- **`app-shell/utils/managedByEmptyState.ts`** — the writable-`system` create check and its local `EmptyStateUserActions` interface are replaced by `resolveCrudAffordances({ managedBy, userActions }).create`.
|
|
203
|
+
- **`plugin-grid/rowCrudAffordances.ts`** — the local `isOptedOut` / `predicatesOf` helpers (and duplicated `RowCrudUserAction` / `RowCrudPredicates` types) fold into `normalizeUserAction`; the historical type names stay re-exported for compat.
|
|
204
|
+
- **`plugin-detail/RelatedList.tsx`** — its inline `predicatesOf` fold into `userActionPredicates`.
|
|
205
|
+
- **`plugin-form/ObjectForm.tsx`** — the hand-rolled `managedBy !== 'platform'` blanket lock + `userActions` unlock is replaced by the resolved affordance for the current mode (`edit` / `create`), the **same** `resolveCrudAffordances` contract the detail (`isObjectInlineEditable`) and grid surfaces use.
|
|
206
|
+
|
|
207
|
+
Behavior-preserving for `platform` / `system` / `append-only` / `better-auth`, with one deliberate alignment: an admin-editable **`config`**-bucket object (e.g. `sys_webhook`, `sys_permission_set`) is now editable in `ObjectForm` — it was previously over-locked as "non-`platform`", while detail/grid already treated it as editable (`config` resolves `edit: true`). New unit coverage for the shared parser and the config / create-mode form gate; all existing affordance/edit-gate tests stay green.
|
|
208
|
+
|
|
209
|
+
- eee4ded: feat(fields): render `select` + `multiple` through the multi-value chip picker; restore fields/core lint gates
|
|
210
|
+
|
|
211
|
+
- **Multi-value select** — a `select` field/param declared `multiple: true`
|
|
212
|
+
now renders the multi-value chip picker (the `multiselect` widget) and stores
|
|
213
|
+
a `string[]`, instead of collapsing to a single-value dropdown that could
|
|
214
|
+
hold only one value. The delegation lives inside `SelectField`, so the object
|
|
215
|
+
form, the inline grid editor, and the app-shell `ActionParamDialog` all
|
|
216
|
+
inherit it from the one `select` widget with no per-surface drift. Single
|
|
217
|
+
selects keep the cascading dropdown (multi + per-option `visibleWhen`
|
|
218
|
+
cascading is not a combination in use today).
|
|
219
|
+
- **`autonumber` mapping is unchanged** here; this change is orthogonal.
|
|
220
|
+
- **Lint gates restored** — fixed the pre-existing baseline lint errors that
|
|
221
|
+
had left the `@object-ui/fields` and `@object-ui/core` package lints red (so
|
|
222
|
+
the gate could not catch new violations): `react-hooks/rules-of-hooks` in
|
|
223
|
+
`ImageField` / `TextAreaField` / `index.tsx` (hooks hoisted above early
|
|
224
|
+
returns; the `useFieldTranslate` hook no longer wrapped in try/catch), plus
|
|
225
|
+
`no-useless-assignment` / `no-useless-escape` / `no-control-regex` /
|
|
226
|
+
`prefer-const` / `preserve-caught-error` in the core evaluator and utils. No
|
|
227
|
+
behavior change from the lint fixes.
|
|
228
|
+
|
|
229
|
+
- Updated dependencies [7cf4051]
|
|
230
|
+
- Updated dependencies [94d4876]
|
|
231
|
+
- Updated dependencies [2b17339]
|
|
232
|
+
- Updated dependencies [31b77d4]
|
|
233
|
+
- Updated dependencies [6d4fbe6]
|
|
234
|
+
- Updated dependencies [62b9ab5]
|
|
235
|
+
- Updated dependencies [29c6040]
|
|
236
|
+
- Updated dependencies [faebac3]
|
|
237
|
+
- Updated dependencies [199fa83]
|
|
238
|
+
- @object-ui/types@16.1.0
|
|
239
|
+
|
|
240
|
+
## 16.0.0
|
|
241
|
+
|
|
242
|
+
### Patch Changes
|
|
243
|
+
|
|
244
|
+
- Updated dependencies [210806a]
|
|
245
|
+
- Updated dependencies [b4ef588]
|
|
246
|
+
- Updated dependencies [5534535]
|
|
247
|
+
- Updated dependencies [9b8f978]
|
|
248
|
+
- @object-ui/types@16.0.0
|
|
249
|
+
|
|
3
250
|
## 15.0.0
|
|
4
251
|
|
|
5
252
|
### Patch Changes
|
|
@@ -258,6 +258,12 @@ export interface ActionParamDef {
|
|
|
258
258
|
* gated on `features.phoneNumber`). Absent = always visible.
|
|
259
259
|
*/
|
|
260
260
|
visible?: string;
|
|
261
|
+
/** Accepted upload types (MIME types / extensions) for `file`/`image` params. */
|
|
262
|
+
accept?: string[];
|
|
263
|
+
/** Max upload size in bytes for `file`/`image` params. */
|
|
264
|
+
maxSize?: number;
|
|
265
|
+
/** Allow multiple values (file/image/lookup/user params → array value). */
|
|
266
|
+
multiple?: boolean;
|
|
261
267
|
/** Object name the lookup picker queries (`reference_to` on the field). */
|
|
262
268
|
referenceTo?: string;
|
|
263
269
|
/** Field on the referenced record used as the human label (default `name`). */
|
|
@@ -266,8 +272,6 @@ export interface ActionParamDef {
|
|
|
266
272
|
idField?: string;
|
|
267
273
|
/** Optional secondary line shown under the label in the picker. */
|
|
268
274
|
descriptionField?: string;
|
|
269
|
-
/** Allow multi-select (renders chip list). */
|
|
270
|
-
multiple?: boolean;
|
|
271
275
|
/** Template (e.g. `{first_name} {last_name}`) used to build the label. */
|
|
272
276
|
titleFormat?: string;
|
|
273
277
|
/** Column definitions surfaced in the enterprise `RecordPickerDialog`. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* A DataSource adapter for the `provider: 'api'` ViewData mode.
|
|
9
9
|
* Makes raw HTTP requests using the HttpRequest configs from ViewData.
|
|
10
10
|
*/
|
|
11
|
-
import type { DataSource, QueryParams, QueryResult, HttpRequest, AggregateParams, AggregateResult } from '@object-ui/types';
|
|
11
|
+
import type { DataSource, BatchTransactionOperation, QueryParams, QueryResult, HttpRequest, AggregateParams, AggregateResult } from '@object-ui/types';
|
|
12
12
|
export interface ApiDataSourceConfig {
|
|
13
13
|
/** HttpRequest config for read operations (find, findOne) */
|
|
14
14
|
read?: HttpRequest;
|
|
@@ -54,6 +54,16 @@ export declare class ApiDataSource<T = any> implements DataSource<T> {
|
|
|
54
54
|
delete(_resource: string, id: string | number, opts?: {
|
|
55
55
|
ifMatch?: string;
|
|
56
56
|
}): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Client-side (non-atomic) cross-object batch — a generic REST endpoint has
|
|
59
|
+
* no transactional batch primitive, so this delegates to the shared
|
|
60
|
+
* emulation (sequential writes + `$ref` resolution + best-effort
|
|
61
|
+
* compensation). Adapters fronting a server that DOES offer an atomic batch
|
|
62
|
+
* should override this to issue a single request.
|
|
63
|
+
*/
|
|
64
|
+
batchTransaction(operations: BatchTransactionOperation[]): Promise<{
|
|
65
|
+
results: any[];
|
|
66
|
+
}>;
|
|
57
67
|
getObjectSchema(_objectName: string): Promise<any>;
|
|
58
68
|
getView(_objectName: string, _viewId: string): Promise<any | null>;
|
|
59
69
|
getApp(_appId: string): Promise<any | null>;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* A DataSource adapter for the `provider: 'api'` ViewData mode.
|
|
9
9
|
* Makes raw HTTP requests using the HttpRequest configs from ViewData.
|
|
10
10
|
*/
|
|
11
|
+
import { emulateBatchTransaction } from './batchTransaction.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -241,6 +242,16 @@ export class ApiDataSource {
|
|
|
241
242
|
return false;
|
|
242
243
|
}
|
|
243
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Client-side (non-atomic) cross-object batch — a generic REST endpoint has
|
|
247
|
+
* no transactional batch primitive, so this delegates to the shared
|
|
248
|
+
* emulation (sequential writes + `$ref` resolution + best-effort
|
|
249
|
+
* compensation). Adapters fronting a server that DOES offer an atomic batch
|
|
250
|
+
* should override this to issue a single request.
|
|
251
|
+
*/
|
|
252
|
+
batchTransaction(operations) {
|
|
253
|
+
return emulateBatchTransaction(this, operations);
|
|
254
|
+
}
|
|
244
255
|
async getObjectSchema(_objectName) {
|
|
245
256
|
// Generic API endpoints typically don't expose schema metadata.
|
|
246
257
|
// Return a minimal stub so schema-dependent components don't crash.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* A DataSource adapter for the `provider: 'value'` ViewData mode.
|
|
9
9
|
* Operates entirely on an in-memory array — no network requests.
|
|
10
10
|
*/
|
|
11
|
-
import type { DataSource, MutationEvent, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
|
|
11
|
+
import type { DataSource, BatchTransactionOperation, MutationEvent, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
|
|
12
12
|
export interface ValueDataSourceConfig<T = any> {
|
|
13
13
|
/** The static data array */
|
|
14
14
|
items: T[];
|
|
@@ -48,6 +48,14 @@ export declare class ValueDataSource<T = any> implements DataSource<T> {
|
|
|
48
48
|
update(_resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
|
49
49
|
delete(_resource: string, id: string | number): Promise<boolean>;
|
|
50
50
|
bulk(_resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Client-side (non-atomic) cross-object batch — this in-memory adapter has
|
|
53
|
+
* no server transaction, so it delegates to the shared emulation. Per-op
|
|
54
|
+
* MutationEvents come from the `create`/`update`/`delete` primitives above.
|
|
55
|
+
*/
|
|
56
|
+
batchTransaction(operations: BatchTransactionOperation[]): Promise<{
|
|
57
|
+
results: any[];
|
|
58
|
+
}>;
|
|
51
59
|
getObjectSchema(_objectName: string): Promise<any>;
|
|
52
60
|
getView(_objectName: string, _viewId: string): Promise<any | null>;
|
|
53
61
|
getApp(_appId: string): Promise<any | null>;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* A DataSource adapter for the `provider: 'value'` ViewData mode.
|
|
9
9
|
* Operates entirely on an in-memory array — no network requests.
|
|
10
10
|
*/
|
|
11
|
+
import { emulateBatchTransaction } from './batchTransaction.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -341,6 +342,14 @@ export class ValueDataSource {
|
|
|
341
342
|
}
|
|
342
343
|
return results;
|
|
343
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Client-side (non-atomic) cross-object batch — this in-memory adapter has
|
|
347
|
+
* no server transaction, so it delegates to the shared emulation. Per-op
|
|
348
|
+
* MutationEvents come from the `create`/`update`/`delete` primitives above.
|
|
349
|
+
*/
|
|
350
|
+
batchTransaction(operations) {
|
|
351
|
+
return emulateBatchTransaction(this, operations);
|
|
352
|
+
}
|
|
344
353
|
async getObjectSchema(_objectName) {
|
|
345
354
|
// Infer a minimal schema from the first item
|
|
346
355
|
if (this.items.length === 0)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — batchTransaction emulation
|
|
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
|
+
* Client-side emulation of {@link DataSource.batchTransaction} for adapters
|
|
9
|
+
* that lack a server-side transactional batch endpoint. This is the single,
|
|
10
|
+
* tested home for the non-atomic fallback (ObjectStack objectui #2679 /
|
|
11
|
+
* framework #1604 / ADR-0034 item 4): components call `runBatchTransaction`
|
|
12
|
+
* unconditionally and never orchestrate cross-object writes themselves.
|
|
13
|
+
*
|
|
14
|
+
* The emulation runs operations sequentially — NOT the old `bulk('create')`
|
|
15
|
+
* grouping — because sequential execution is what makes `{ $ref: i }`
|
|
16
|
+
* resolution and ordered semantics correct (a child create must observe the
|
|
17
|
+
* parent's freshly-minted id). Line-item counts are small, so the extra
|
|
18
|
+
* round-trips are acceptable.
|
|
19
|
+
*/
|
|
20
|
+
import type { BatchTransactionOperation, DataSource } from '@object-ui/types';
|
|
21
|
+
/**
|
|
22
|
+
* Emulate an atomic batch over a plain DataSource by executing its `create`/
|
|
23
|
+
* `update`/`delete` primitives in order.
|
|
24
|
+
*
|
|
25
|
+
* NOT atomic: on failure it best-effort compensates by deleting the records
|
|
26
|
+
* it created (reverse order — children before parent), then rethrows the
|
|
27
|
+
* original error. Updates and deletes that already ran cannot be undone; a
|
|
28
|
+
* compensation delete that itself fails is swallowed (`Promise.allSettled`).
|
|
29
|
+
* This is the documented reason a server-backed `batchTransaction` is
|
|
30
|
+
* strictly better — a true transaction simply never commits.
|
|
31
|
+
*
|
|
32
|
+
* MutationEvents are deliberately NOT emitted here: the underlying
|
|
33
|
+
* `create`/`update`/`delete` primitives already emit per-op on adapters that
|
|
34
|
+
* support `onMutation`, so emitting here too would double-fire.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emulateBatchTransaction<T = any>(dataSource: DataSource<T>, operations: BatchTransactionOperation[]): Promise<{
|
|
37
|
+
results: any[];
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Persist an ordered cross-object batch, preferring the adapter's native
|
|
41
|
+
* (potentially atomic) `batchTransaction` and falling back to
|
|
42
|
+
* {@link emulateBatchTransaction} when the adapter does not implement it.
|
|
43
|
+
*
|
|
44
|
+
* This is the single entry point UI components should call — they stay
|
|
45
|
+
* ignorant of whether the underlying save is truly atomic.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runBatchTransaction<T = any>(dataSource: DataSource<T>, operations: BatchTransactionOperation[]): Promise<{
|
|
48
|
+
results: any[];
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — batchTransaction emulation
|
|
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
|
+
* Client-side emulation of {@link DataSource.batchTransaction} for adapters
|
|
9
|
+
* that lack a server-side transactional batch endpoint. This is the single,
|
|
10
|
+
* tested home for the non-atomic fallback (ObjectStack objectui #2679 /
|
|
11
|
+
* framework #1604 / ADR-0034 item 4): components call `runBatchTransaction`
|
|
12
|
+
* unconditionally and never orchestrate cross-object writes themselves.
|
|
13
|
+
*
|
|
14
|
+
* The emulation runs operations sequentially — NOT the old `bulk('create')`
|
|
15
|
+
* grouping — because sequential execution is what makes `{ $ref: i }`
|
|
16
|
+
* resolution and ordered semantics correct (a child create must observe the
|
|
17
|
+
* parent's freshly-minted id). Line-item counts are small, so the extra
|
|
18
|
+
* round-trips are acceptable.
|
|
19
|
+
*/
|
|
20
|
+
/** Best-effort id extraction across the record shapes adapters return. */
|
|
21
|
+
function idOf(rec) {
|
|
22
|
+
if (rec == null)
|
|
23
|
+
return undefined;
|
|
24
|
+
return rec.id ?? rec._id ?? rec.recordId;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Replace top-level `{ $ref: i }` values in an op payload with the id produced
|
|
28
|
+
* by operation `i`. Only backward references are valid — a forward or
|
|
29
|
+
* unresolved ref throws (the builders only ever emit `{ $ref: 0 }` pointing at
|
|
30
|
+
* an already-executed parent, so this is a guard, not a normal path).
|
|
31
|
+
*/
|
|
32
|
+
function resolveRefs(data, results) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, value] of Object.entries(data)) {
|
|
35
|
+
if (value &&
|
|
36
|
+
typeof value === 'object' &&
|
|
37
|
+
!Array.isArray(value) &&
|
|
38
|
+
typeof value.$ref === 'number') {
|
|
39
|
+
const refIndex = value.$ref;
|
|
40
|
+
const id = idOf(results[refIndex]);
|
|
41
|
+
if (id === undefined) {
|
|
42
|
+
throw new Error(`batchTransaction: field "${key}" references operation ${refIndex}, ` +
|
|
43
|
+
`which has not produced an id (forward or invalid $ref)`);
|
|
44
|
+
}
|
|
45
|
+
out[key] = id;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
out[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Emulate an atomic batch over a plain DataSource by executing its `create`/
|
|
55
|
+
* `update`/`delete` primitives in order.
|
|
56
|
+
*
|
|
57
|
+
* NOT atomic: on failure it best-effort compensates by deleting the records
|
|
58
|
+
* it created (reverse order — children before parent), then rethrows the
|
|
59
|
+
* original error. Updates and deletes that already ran cannot be undone; a
|
|
60
|
+
* compensation delete that itself fails is swallowed (`Promise.allSettled`).
|
|
61
|
+
* This is the documented reason a server-backed `batchTransaction` is
|
|
62
|
+
* strictly better — a true transaction simply never commits.
|
|
63
|
+
*
|
|
64
|
+
* MutationEvents are deliberately NOT emitted here: the underlying
|
|
65
|
+
* `create`/`update`/`delete` primitives already emit per-op on adapters that
|
|
66
|
+
* support `onMutation`, so emitting here too would double-fire.
|
|
67
|
+
*/
|
|
68
|
+
export async function emulateBatchTransaction(dataSource, operations) {
|
|
69
|
+
const results = [];
|
|
70
|
+
const created = [];
|
|
71
|
+
try {
|
|
72
|
+
for (const op of operations) {
|
|
73
|
+
const action = op.action ?? 'create';
|
|
74
|
+
if (action === 'create') {
|
|
75
|
+
const record = await dataSource.create(op.object, resolveRefs(op.data ?? {}, results));
|
|
76
|
+
results.push(record);
|
|
77
|
+
const id = idOf(record);
|
|
78
|
+
if (id)
|
|
79
|
+
created.push({ object: op.object, id });
|
|
80
|
+
}
|
|
81
|
+
else if (action === 'update') {
|
|
82
|
+
if (op.id == null) {
|
|
83
|
+
throw new Error(`batchTransaction: update on "${op.object}" requires an id`);
|
|
84
|
+
}
|
|
85
|
+
results.push(await dataSource.update(op.object, op.id, resolveRefs(op.data ?? {}, results)));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (op.id == null) {
|
|
89
|
+
throw new Error(`batchTransaction: delete on "${op.object}" requires an id`);
|
|
90
|
+
}
|
|
91
|
+
results.push(await dataSource.delete(op.object, op.id));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { results };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// Best-effort compensation: undo the creates we made, newest first, so a
|
|
98
|
+
// child is removed before the parent it points at. Failures are ignored —
|
|
99
|
+
// there is nothing more we can do client-side.
|
|
100
|
+
if (created.length > 0) {
|
|
101
|
+
await Promise.allSettled([...created].reverse().map((c) => dataSource.delete(c.object, c.id)));
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Persist an ordered cross-object batch, preferring the adapter's native
|
|
108
|
+
* (potentially atomic) `batchTransaction` and falling back to
|
|
109
|
+
* {@link emulateBatchTransaction} when the adapter does not implement it.
|
|
110
|
+
*
|
|
111
|
+
* This is the single entry point UI components should call — they stay
|
|
112
|
+
* ignorant of whether the underlying save is truly atomic.
|
|
113
|
+
*/
|
|
114
|
+
export function runBatchTransaction(dataSource, operations) {
|
|
115
|
+
return typeof dataSource.batchTransaction === 'function'
|
|
116
|
+
? dataSource.batchTransaction(operations)
|
|
117
|
+
: emulateBatchTransaction(dataSource, operations);
|
|
118
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -8,3 +8,4 @@
|
|
|
8
8
|
export { ApiDataSource, type ApiDataSourceConfig } from './ApiDataSource.js';
|
|
9
9
|
export { ValueDataSource, type ValueDataSourceConfig } from './ValueDataSource.js';
|
|
10
10
|
export { resolveDataSource, type ResolveDataSourceOptions } from './resolveDataSource.js';
|
|
11
|
+
export { emulateBatchTransaction, runBatchTransaction } from './batchTransaction.js';
|
package/dist/adapters/index.js
CHANGED
|
@@ -86,6 +86,16 @@ export declare class ExpressionEvaluator {
|
|
|
86
86
|
dialect?: string;
|
|
87
87
|
source?: string;
|
|
88
88
|
}, options?: EvaluationOptions): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Evaluate a `{ dialect: 'cel' }` predicate on the canonical `@objectstack/formula`
|
|
91
|
+
* engine (via `evalFieldPredicate`), binding this evaluator's context: the
|
|
92
|
+
* `record` key as the `record` namespace and the whole context bag as top-level
|
|
93
|
+
* scope so `record.*`, `features.*`, `user.*`, `app.*` all resolve. Fail-soft to
|
|
94
|
+
* `true` (visible/enabled — the legacy default) unless the caller opted into
|
|
95
|
+
* `throwOnError`, in which case a *faulting* predicate (bad field / non-CEL
|
|
96
|
+
* syntax) throws; a genuine `false` never throws.
|
|
97
|
+
*/
|
|
98
|
+
private evaluateCelCondition;
|
|
89
99
|
/**
|
|
90
100
|
* Update the context with new data
|
|
91
101
|
*/
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { ExpressionContext } from './ExpressionContext.js';
|
|
18
18
|
import { ExpressionCache } from './ExpressionCache.js';
|
|
19
19
|
import { FormulaFunctions } from './FormulaFunctions.js';
|
|
20
|
+
import { evalFieldPredicate } from './fieldRules.js';
|
|
20
21
|
/**
|
|
21
22
|
* Expression evaluator for dynamic UI expressions
|
|
22
23
|
*/
|
|
@@ -141,6 +142,10 @@ export class ExpressionEvaluator {
|
|
|
141
142
|
return compiled.fn(...varValues);
|
|
142
143
|
}
|
|
143
144
|
catch (error) {
|
|
145
|
+
// The original error's message is inlined below. We can't pass it as the
|
|
146
|
+
// `Error` `cause` option because this package targets ES2020, whose lib
|
|
147
|
+
// types the 1-arg `Error` constructor only; hence the scoped disable.
|
|
148
|
+
// eslint-disable-next-line preserve-caught-error
|
|
144
149
|
throw new Error(`Failed to evaluate expression "${expression}": ${error.message}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -177,6 +182,18 @@ export class ExpressionEvaluator {
|
|
|
177
182
|
if (typeof condition === 'boolean') {
|
|
178
183
|
return condition;
|
|
179
184
|
}
|
|
185
|
+
// #2661 — a CEL-dialect envelope routes to the canonical `@objectstack/formula`
|
|
186
|
+
// engine (the one `fieldRules` / list conditionals already use), NOT the legacy
|
|
187
|
+
// JS evaluator below. This makes a component / action `visible` / `disabled`
|
|
188
|
+
// predicate reach the SAME verdict as server enforcement — including CEL-only
|
|
189
|
+
// behavior like `record.due_date == today()` (framework#3205). Bare strings and
|
|
190
|
+
// `${…}` templates stay on the legacy path (back-compat deprecation window);
|
|
191
|
+
// only an explicit `{ dialect: 'cel' }` envelope is rerouted.
|
|
192
|
+
if (condition && typeof condition === 'object'
|
|
193
|
+
&& condition.dialect === 'cel'
|
|
194
|
+
&& typeof condition.source === 'string') {
|
|
195
|
+
return this.evaluateCelCondition(condition.source, options);
|
|
196
|
+
}
|
|
180
197
|
// Unwrap Expression envelope (see `evaluate` for rationale).
|
|
181
198
|
if (condition && typeof condition === 'object' && typeof condition.source === 'string') {
|
|
182
199
|
condition = condition.source;
|
|
@@ -215,6 +232,37 @@ export class ExpressionEvaluator {
|
|
|
215
232
|
return true;
|
|
216
233
|
}
|
|
217
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Evaluate a `{ dialect: 'cel' }` predicate on the canonical `@objectstack/formula`
|
|
237
|
+
* engine (via `evalFieldPredicate`), binding this evaluator's context: the
|
|
238
|
+
* `record` key as the `record` namespace and the whole context bag as top-level
|
|
239
|
+
* scope so `record.*`, `features.*`, `user.*`, `app.*` all resolve. Fail-soft to
|
|
240
|
+
* `true` (visible/enabled — the legacy default) unless the caller opted into
|
|
241
|
+
* `throwOnError`, in which case a *faulting* predicate (bad field / non-CEL
|
|
242
|
+
* syntax) throws; a genuine `false` never throws.
|
|
243
|
+
*/
|
|
244
|
+
evaluateCelCondition(source, options) {
|
|
245
|
+
if (!source.trim())
|
|
246
|
+
return true; // no predicate → visible/enabled
|
|
247
|
+
const bag = this.context.toObject();
|
|
248
|
+
const rec = bag.record;
|
|
249
|
+
const record = (rec && typeof rec === 'object' && !Array.isArray(rec))
|
|
250
|
+
? rec
|
|
251
|
+
: bag;
|
|
252
|
+
if (!options.throwOnError) {
|
|
253
|
+
// Fast path: one evaluation, fail-soft to visible/enabled (legacy parity).
|
|
254
|
+
return evalFieldPredicate(source, record, true, undefined, bag);
|
|
255
|
+
}
|
|
256
|
+
// Fail-closed callers need to tell a genuine `false` from a fault. The
|
|
257
|
+
// canonical helper fails soft to the fallback, so a value that tracks the
|
|
258
|
+
// fallback in BOTH runs means the predicate faulted — then we throw.
|
|
259
|
+
const asTrue = evalFieldPredicate(source, record, true, undefined, bag);
|
|
260
|
+
const asFalse = evalFieldPredicate(source, record, false, undefined, bag);
|
|
261
|
+
if (asTrue !== asFalse) {
|
|
262
|
+
throw new Error(`CEL predicate failed to evaluate: ${source}`);
|
|
263
|
+
}
|
|
264
|
+
return asTrue;
|
|
265
|
+
}
|
|
218
266
|
/**
|
|
219
267
|
* Update the context with new data
|
|
220
268
|
*/
|
|
@@ -298,7 +298,10 @@ export class SafeExpressionParser {
|
|
|
298
298
|
this.skipWhitespace();
|
|
299
299
|
// eslint-disable-next-line no-constant-condition
|
|
300
300
|
while (true) {
|
|
301
|
-
|
|
301
|
+
// Every branch below either assigns `op` or `break`s out of the loop, so
|
|
302
|
+
// `op` is always set by the time the switch reads it (no null initializer
|
|
303
|
+
// needed).
|
|
304
|
+
let op;
|
|
302
305
|
if (this.peek() === '=' && this.peek(1) === '=' && this.peek(2) === '=') {
|
|
303
306
|
op = '===';
|
|
304
307
|
this.pos += 3;
|
|
@@ -648,7 +651,7 @@ export class SafeExpressionParser {
|
|
|
648
651
|
// Optional exponent: e+5, E-3
|
|
649
652
|
if (/[eE]/.test(this.source[this.pos] ?? '')) {
|
|
650
653
|
this.pos++; // consume 'e' or 'E'
|
|
651
|
-
if (/[
|
|
654
|
+
if (/[+-]/.test(this.source[this.pos] ?? ''))
|
|
652
655
|
this.pos++; // optional sign
|
|
653
656
|
let expDigits = 0;
|
|
654
657
|
while (this.pos < this.source.length && /\d/.test(this.source[this.pos])) {
|
|
@@ -68,3 +68,25 @@ export declare function resolveVisibleOptions<T extends OptionLike>(options: rea
|
|
|
68
68
|
* no longer offered). An empty value is always "valid" (nothing to clear).
|
|
69
69
|
*/
|
|
70
70
|
export declare function isValueStillOffered(value: unknown, visibleOptions: readonly OptionLike[]): boolean;
|
|
71
|
+
/** The resolved state of a cascading / role-gated option list. */
|
|
72
|
+
export interface CascadingOptions<T extends OptionLike> {
|
|
73
|
+
/** Offered options after `visibleWhen` filtering. Empty while gated. */
|
|
74
|
+
options: T[];
|
|
75
|
+
/** True when a declared `dependsOn` field is empty — the list is gated. */
|
|
76
|
+
gated: boolean;
|
|
77
|
+
/** Normalized `dependsOn` field names (drives the "select the parent first" hint). */
|
|
78
|
+
dependsOnFields: string[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve an option field's offered set in one shot: normalize `dependsOn`,
|
|
82
|
+
* decide whether the list is gated (a controlling field is still empty), and —
|
|
83
|
+
* unless gated — filter by each option's `visibleWhen`. The single source of
|
|
84
|
+
* truth behind the option widgets' `useCascadingOptions` hook and the form
|
|
85
|
+
* renderer's inline pre-filter / cascade-clear, so gating and filtering can
|
|
86
|
+
* never drift between them (#2715).
|
|
87
|
+
*
|
|
88
|
+
* `gated` is true only when there IS a `dependsOn` and one of its fields is
|
|
89
|
+
* empty; a field with no `dependsOn` is never gated (its options are filtered
|
|
90
|
+
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveCascadingOptions<T extends OptionLike>(rawOptions: readonly T[] | undefined | null, record: Record<string, unknown>, dependsOn: DependsOnInput, scope?: Record<string, unknown>): CascadingOptions<T>;
|
|
@@ -78,3 +78,21 @@ export function isValueStillOffered(value, visibleOptions) {
|
|
|
78
78
|
}
|
|
79
79
|
return visibleOptions.some((o) => o.value === value);
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve an option field's offered set in one shot: normalize `dependsOn`,
|
|
83
|
+
* decide whether the list is gated (a controlling field is still empty), and —
|
|
84
|
+
* unless gated — filter by each option's `visibleWhen`. The single source of
|
|
85
|
+
* truth behind the option widgets' `useCascadingOptions` hook and the form
|
|
86
|
+
* renderer's inline pre-filter / cascade-clear, so gating and filtering can
|
|
87
|
+
* never drift between them (#2715).
|
|
88
|
+
*
|
|
89
|
+
* `gated` is true only when there IS a `dependsOn` and one of its fields is
|
|
90
|
+
* empty; a field with no `dependsOn` is never gated (its options are filtered
|
|
91
|
+
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
|
|
94
|
+
const dependsOnFields = resolveDependsOnFields(dependsOn);
|
|
95
|
+
const gated = dependsOnFields.length > 0 && isOptionGroupGated(dependsOn, record);
|
|
96
|
+
const options = gated ? [] : resolveVisibleOptions(rawOptions, record, scope);
|
|
97
|
+
return { options, gated, dependsOnFields };
|
|
98
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './registry/WidgetRegistry.js';
|
|
|
14
14
|
export * from './validation/index.js';
|
|
15
15
|
export * from './builder/schema-builder.js';
|
|
16
16
|
export * from './utils/filter-converter.js';
|
|
17
|
+
export * from './utils/managedBy.js';
|
|
17
18
|
export * from './utils/extract-records.js';
|
|
18
19
|
export * from './utils/expand-fields.js';
|
|
19
20
|
export * from './evaluator/index.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './registry/WidgetRegistry.js';
|
|
|
13
13
|
export * from './validation/index.js';
|
|
14
14
|
export * from './builder/schema-builder.js';
|
|
15
15
|
export * from './utils/filter-converter.js';
|
|
16
|
+
export * from './utils/managedBy.js';
|
|
16
17
|
export * from './utils/extract-records.js';
|
|
17
18
|
export * from './utils/expand-fields.js';
|
|
18
19
|
export * from './evaluator/index.js';
|
|
@@ -69,6 +69,16 @@ export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] |
|
|
|
69
69
|
measureField: (name: string) => DatasetResultField | undefined;
|
|
70
70
|
headerLabel: (name: string) => string;
|
|
71
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* A half-open date-range drill scope for one time-bucketed dimension (#1752):
|
|
74
|
+
* the object FIELD to filter and its inclusive `gte` / exclusive `lt` bounds
|
|
75
|
+
* (the server's `drillRanges` sidecar entry).
|
|
76
|
+
*/
|
|
77
|
+
export interface DatasetDrillRange {
|
|
78
|
+
field: string;
|
|
79
|
+
gte: unknown;
|
|
80
|
+
lt: unknown;
|
|
81
|
+
}
|
|
72
82
|
/**
|
|
73
83
|
* Build the record-list filter for a drilled dataset bucket (ADR-0021 D2).
|
|
74
84
|
*
|
|
@@ -79,7 +89,14 @@ export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] |
|
|
|
79
89
|
* "is empty" filter). The render-time `runtimeFilter` is ANDed in so the drilled
|
|
80
90
|
* list stays within the same slice the aggregate was computed over.
|
|
81
91
|
*
|
|
92
|
+
* A time-bucketed date dimension (#1752) drills by RANGE, not equality — a
|
|
93
|
+
* humanized bucket ("2026-Q2") can't be exact-matched, so the server sends a
|
|
94
|
+
* half-open `[gte, lt)` per date dim in `rawRanges` instead of a raw value.
|
|
95
|
+
* Each becomes an ObjectQL range operator object (`{ $gte, $lt }`) so the drill
|
|
96
|
+
* scopes the list to the clicked time bucket instead of every bucket (which the
|
|
97
|
+
* old date-dim skip degraded to — a superset).
|
|
98
|
+
*
|
|
82
99
|
* Shared by the dashboard `DatasetWidget` and the report renderer so a drill
|
|
83
100
|
* filters identically (and correctly, including lookups) on both surfaces.
|
|
84
101
|
*/
|
|
85
|
-
export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?: Record<string, unknown>): Record<string, unknown>;
|
|
102
|
+
export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?: Record<string, unknown>, rawRanges?: Record<string, DatasetDrillRange>): Record<string, unknown>;
|
|
@@ -104,14 +104,27 @@ export function buildDatasetFieldHelpers(fields, object, fieldLabel) {
|
|
|
104
104
|
* "is empty" filter). The render-time `runtimeFilter` is ANDed in so the drilled
|
|
105
105
|
* list stays within the same slice the aggregate was computed over.
|
|
106
106
|
*
|
|
107
|
+
* A time-bucketed date dimension (#1752) drills by RANGE, not equality — a
|
|
108
|
+
* humanized bucket ("2026-Q2") can't be exact-matched, so the server sends a
|
|
109
|
+
* half-open `[gte, lt)` per date dim in `rawRanges` instead of a raw value.
|
|
110
|
+
* Each becomes an ObjectQL range operator object (`{ $gte, $lt }`) so the drill
|
|
111
|
+
* scopes the list to the clicked time bucket instead of every bucket (which the
|
|
112
|
+
* old date-dim skip degraded to — a superset).
|
|
113
|
+
*
|
|
107
114
|
* Shared by the dashboard `DatasetWidget` and the report renderer so a drill
|
|
108
115
|
* filters identically (and correctly, including lookups) on both surfaces.
|
|
109
116
|
*/
|
|
110
|
-
export function buildDatasetDrillFilter(rawRow, drillDims, dimensionFields, runtimeFilter) {
|
|
117
|
+
export function buildDatasetDrillFilter(rawRow, drillDims, dimensionFields, runtimeFilter, rawRanges) {
|
|
111
118
|
const drillFilter = {};
|
|
112
119
|
for (const d of drillDims) {
|
|
113
120
|
const raw = rawRow?.[d];
|
|
114
121
|
drillFilter[dimensionFields[d]] = raw === '' || raw === undefined ? null : raw;
|
|
115
122
|
}
|
|
123
|
+
if (rawRanges) {
|
|
124
|
+
for (const r of Object.values(rawRanges)) {
|
|
125
|
+
if (r && r.field)
|
|
126
|
+
drillFilter[r.field] = { $gte: r.gte, $lt: r.lt };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
return runtimeFilter ? { ...runtimeFilter, ...drillFilter } : drillFilter;
|
|
117
130
|
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* translated object label (e.g. 合同) → object API name → 'export'.
|
|
9
9
|
*/
|
|
10
10
|
/** Windows-reserved + control characters that cannot appear in a filename. */
|
|
11
|
-
|
|
11
|
+
// The control-char range in the class is matched deliberately (illegal in
|
|
12
|
+
// filenames, must be stripped), so no-control-regex is intentionally disabled.
|
|
13
|
+
// eslint-disable-next-line no-control-regex
|
|
14
|
+
const ILLEGAL_FILENAME_CHARS = /[\\/:*?"<>|\u0000-\u001f]+/g;
|
|
12
15
|
/** Longest base we emit — keeps the full name comfortably under OS limits. */
|
|
13
16
|
const MAX_BASE_LENGTH = 80;
|
|
14
17
|
/**
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* managedBy — the single source of truth for interpreting an object's
|
|
3
|
+
* lifecycle bucket (`ObjectSchema.managedBy`) into CRUD affordances.
|
|
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.
|
|
12
|
+
*
|
|
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`.
|
|
17
|
+
*/
|
|
18
|
+
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
|
+
}
|
|
44
|
+
/** `edit`/`delete` accept a bare boolean or the #2614 object form. */
|
|
45
|
+
export type UserActionOverride = boolean | {
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
visibleWhen?: unknown;
|
|
48
|
+
disabledWhen?: unknown;
|
|
49
|
+
};
|
|
50
|
+
export interface UserActionsOverride {
|
|
51
|
+
create?: boolean;
|
|
52
|
+
import?: boolean;
|
|
53
|
+
edit?: UserActionOverride;
|
|
54
|
+
delete?: UserActionOverride;
|
|
55
|
+
exportCsv?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface SchemaLike {
|
|
58
|
+
managedBy?: string | null;
|
|
59
|
+
userActions?: UserActionsOverride | null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Collapse an `edit`/`delete` override (boolean or #2614 object form) onto
|
|
63
|
+
* the given bucket default, surfacing any per-record predicates alongside.
|
|
64
|
+
*
|
|
65
|
+
* 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`).
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeUserAction(v: UserActionOverride | undefined | null, base: boolean): {
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
predicates?: RowCrudPredicates;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Extract the per-record CEL predicates (#2614 object form) from a
|
|
78
|
+
* `userActions.edit` / `delete` flag, or `undefined` for a bare boolean /
|
|
79
|
+
* predicate-less flag. Independent of the object-level enabled verdict — row
|
|
80
|
+
* renderers use this to gate a built-in action per record. Thin wrapper over
|
|
81
|
+
* {@link normalizeUserAction} so the parse lives in exactly one place.
|
|
82
|
+
*/
|
|
83
|
+
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;
|
|
86
|
+
/** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
|
|
87
|
+
export declare function isWriteOptedIn(v: UserActionOverride | undefined | null): boolean;
|
|
88
|
+
/**
|
|
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.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isSystemWritable(obj: SchemaLike | null | undefined): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether an object's rows may be edited in place (detail inline-edit / form
|
|
96
|
+
* fields) — the resolved `edit` affordance. Engine-owned `system` /
|
|
97
|
+
* `append-only` / `better-auth` objects resolve to `false` unless they opened
|
|
98
|
+
* `userActions.edit`. (Per-record predicates gate row action buttons, not
|
|
99
|
+
* object-level editability, so only the resolved boolean matters here.)
|
|
100
|
+
*/
|
|
101
|
+
export declare function isObjectInlineEditable(obj: SchemaLike | null | undefined): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* managedBy — the single source of truth for interpreting an object's
|
|
3
|
+
* lifecycle bucket (`ObjectSchema.managedBy`) into CRUD affordances.
|
|
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.
|
|
12
|
+
*
|
|
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`.
|
|
17
|
+
*/
|
|
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
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Collapse an `edit`/`delete` override (boolean or #2614 object form) onto
|
|
28
|
+
* the given bucket default, surfacing any per-record predicates alongside.
|
|
29
|
+
*
|
|
30
|
+
* 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`).
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeUserAction(v, base) {
|
|
38
|
+
if (v == null)
|
|
39
|
+
return { enabled: base };
|
|
40
|
+
if (typeof v === 'boolean')
|
|
41
|
+
return { enabled: v };
|
|
42
|
+
const enabled = v.enabled ?? base;
|
|
43
|
+
if (v.visibleWhen == null && v.disabledWhen == null)
|
|
44
|
+
return { enabled };
|
|
45
|
+
const predicates = {};
|
|
46
|
+
if (v.visibleWhen != null)
|
|
47
|
+
predicates.visibleWhen = v.visibleWhen;
|
|
48
|
+
if (v.disabledWhen != null)
|
|
49
|
+
predicates.disabledWhen = v.disabledWhen;
|
|
50
|
+
return { enabled, predicates };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract the per-record CEL predicates (#2614 object form) from a
|
|
54
|
+
* `userActions.edit` / `delete` flag, or `undefined` for a bare boolean /
|
|
55
|
+
* predicate-less flag. Independent of the object-level enabled verdict — row
|
|
56
|
+
* renderers use this to gate a built-in action per record. Thin wrapper over
|
|
57
|
+
* {@link normalizeUserAction} so the parse lives in exactly one place.
|
|
58
|
+
*/
|
|
59
|
+
export function userActionPredicates(v) {
|
|
60
|
+
return normalizeUserAction(v, false).predicates;
|
|
61
|
+
}
|
|
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;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
|
|
83
|
+
export function isWriteOptedIn(v) {
|
|
84
|
+
return v === true || (typeof v === 'object' && v !== null && v.enabled === true);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
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.
|
|
90
|
+
*/
|
|
91
|
+
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);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether an object's rows may be edited in place (detail inline-edit / form
|
|
99
|
+
* fields) — the resolved `edit` affordance. Engine-owned `system` /
|
|
100
|
+
* `append-only` / `better-auth` objects resolve to `false` unless they opened
|
|
101
|
+
* `userActions.edit`. (Per-record predicates gate row action buttons, not
|
|
102
|
+
* object-level editability, so only the resolved boolean matters here.)
|
|
103
|
+
*/
|
|
104
|
+
export function isObjectInlineEditable(obj) {
|
|
105
|
+
return resolveCrudAffordances(obj).edit;
|
|
106
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.1.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": "^
|
|
29
|
-
"@objectstack/spec": "^
|
|
28
|
+
"@objectstack/formula": "^16.0.0-rc.0",
|
|
29
|
+
"@objectstack/spec": "^16.0.0-rc.0",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "
|
|
32
|
+
"@object-ui/types": "16.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|