@object-ui/core 17.6.0 → 18.0.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 +7260 -0
- package/README.md +228 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +473 -69
- package/dist/actions/ActionRunner.js +792 -125
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +67 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +154 -0
- package/dist/actions/recordDelete.js +110 -0
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +8 -25
- package/dist/data-scope/ViewDataProvider.js +27 -54
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +208 -31
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +105 -0
- package/dist/evaluator/subjectPermissions.js +86 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +83 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +382 -34
- package/dist/registry/Registry.js +247 -12
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +165 -5
- package/dist/utils/chart-series.js +123 -18
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +346 -0
- package/dist/utils/date-display.js +644 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +81 -9
- package/dist/utils/expand-fields.js +81 -9
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +158 -6
- package/dist/utils/filter-converter.js +1098 -24
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +135 -11
- package/dist/utils/normalize-list-view.js +243 -64
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +141 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +225 -83
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -0,0 +1,252 @@
|
|
|
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 affordance-to-grant map (objectui#12082): ONE table naming, for every
|
|
10
|
+
* console affordance that offers a write, the grant the caller must hold for
|
|
11
|
+
* it — and ONE resolver every such affordance reads.
|
|
12
|
+
*
|
|
13
|
+
* ## The family this closes
|
|
14
|
+
*
|
|
15
|
+
* Each console surface used to decide on its own which grant its affordance
|
|
16
|
+
* reads, and each surface that decided wrongly was found and fixed alone
|
|
17
|
+
* (objectui#4296, #10107, #11000, #12047). objectui#12082 is the member that
|
|
18
|
+
* hurt most: a create form gated its fields on the EDIT grant, so a
|
|
19
|
+
* create-only role could not fill the form it was allowed to submit. The same
|
|
20
|
+
* pass found the record header not reading the update grant at all, and a
|
|
21
|
+
* lookup offering "Create new" with no grant read either (objectui#12081). The
|
|
22
|
+
* cause is one shape — an affordance spelling its own permission logic — so
|
|
23
|
+
* the fix is one table every affordance reads, not another one-off repair.
|
|
24
|
+
*
|
|
25
|
+
* ## What a row says
|
|
26
|
+
*
|
|
27
|
+
* `crud` — the resolved CRUD-affordance bit the affordance also needs: the
|
|
28
|
+
* object's managed-object policy (ADR-0103, the spec's
|
|
29
|
+
* `resolveCrudAffordances`) intersected with the server's effective API
|
|
30
|
+
* operation set (`/me/permissions` `apiOperations`, objectstack#3391), both
|
|
31
|
+
* through {@link resolveEffectiveCrudAffordances}. `null` for an affordance
|
|
32
|
+
* whose write does not go through the object's generic data door (an
|
|
33
|
+
* attachment upload goes through the storage route; the import template has
|
|
34
|
+
* its own endpoint), so that door's operation set says nothing about it.
|
|
35
|
+
*
|
|
36
|
+
* `grant` — the caller's object grant it exercises, asked as `can(object,
|
|
37
|
+
* grant)` (`MePermissionsProvider` maps `create` → `allowCreate`, `update` →
|
|
38
|
+
* `allowEdit`, `delete` → `allowDelete`).
|
|
39
|
+
*
|
|
40
|
+
* `field` — for an affordance that offers FIELDS to write, the field-level
|
|
41
|
+
* question asked of each one: `create` for an insert, `write` for an update.
|
|
42
|
+
* They differ only for a field the permission set does not mention, which the
|
|
43
|
+
* server's field step lets through so that object admission decides: on an
|
|
44
|
+
* insert that is `allowCreate`, on an update `allowEdit`. A field the set marks
|
|
45
|
+
* `editable: false` is refused on both, and the resolver answers that the same
|
|
46
|
+
* way for both questions. So a create form follows what the server enforces on
|
|
47
|
+
* insert, and adds no rule the server does not have.
|
|
48
|
+
*
|
|
49
|
+
* The `write` question is the server's whole update rule for the field, so it
|
|
50
|
+
* also carries the one field whose update needs an OBJECT grant beyond
|
|
51
|
+
* `allowEdit`: the record's owner, the field the spec's `SystemFieldName`
|
|
52
|
+
* constant names `OWNER_ID`. The server refuses an update that writes it without
|
|
53
|
+
* the transfer grant (`allowTransfer`, or `modifyAllRecords`), so the resolver
|
|
54
|
+
* answers `write` for it with that grant too (objectui#12103). Every row that
|
|
55
|
+
* asks `write` therefore offers the owner field only to a caller who may
|
|
56
|
+
* transfer the record — the in-place edit, the list's in-cell edit and an edit
|
|
57
|
+
* form alike, with no row of its own to forget.
|
|
58
|
+
*
|
|
59
|
+
* ## The verdict
|
|
60
|
+
*
|
|
61
|
+
* An affordance shows when the managed-object policy, the effective API
|
|
62
|
+
* operation set AND the caller's grant all allow it ({@link resolveAffordance}).
|
|
63
|
+
* Its `userActions` predicates (the #2614 object form) are surfaced only then:
|
|
64
|
+
* a predicate narrows, and never re-opens what policy, operation set or grant
|
|
65
|
+
* closed.
|
|
66
|
+
*
|
|
67
|
+
* ## Fail-open, as before
|
|
68
|
+
*
|
|
69
|
+
* With no permission provider mounted, `usePermissions()` answers `can` with
|
|
70
|
+
* `true` and `isLoaded` with `false`, so every grant here reads open and the
|
|
71
|
+
* field question is not asked at all — a standalone embed, a designer preview
|
|
72
|
+
* and a public surface behave as they did before permissions existed, and the
|
|
73
|
+
* server still enforces (the `fieldWriteGate.ts` contract in
|
|
74
|
+
* `@object-ui/plugin-form`). The policy half is not a per-principal answer and
|
|
75
|
+
* does not fail open with it, exactly as `resolveEffectiveCrudAffordances`
|
|
76
|
+
* never did.
|
|
77
|
+
*
|
|
78
|
+
* ## Adding an affordance
|
|
79
|
+
*
|
|
80
|
+
* Add its row here and read it through {@link resolveAffordance} (or
|
|
81
|
+
* {@link resolveFieldAffordance} for fields). The enumeration pin in
|
|
82
|
+
* `@object-ui/plugin-form` (`affordanceGrantMap-12082.test.tsx`) runs every row
|
|
83
|
+
* against four grant shapes and holds the row set to its own expectation table,
|
|
84
|
+
* and its census refuses a console source file that reads a CRUD grant without
|
|
85
|
+
* this map.
|
|
86
|
+
*
|
|
87
|
+
* An affordance that reads NO grant is invisible to a census of grant reads —
|
|
88
|
+
* the calendar's quick-create and drag-to-reschedule and the kanban card move
|
|
89
|
+
* were such affordances until they gained their rows. So the same pin also
|
|
90
|
+
* counts the WRITES: every call site that invokes a create, update or delete on
|
|
91
|
+
* a data source has an entry there naming the row it sits behind and the file
|
|
92
|
+
* that reads it, or the reason it has none. A write site added without an entry
|
|
93
|
+
* turns that pin red.
|
|
94
|
+
*/
|
|
95
|
+
import { resolveEffectiveCrudAffordances, } from './managedBy.js';
|
|
96
|
+
/**
|
|
97
|
+
* Every console affordance that offers a write → the grant it reads. Each
|
|
98
|
+
* row's comment names the affordance and the object the grant is asked of;
|
|
99
|
+
* where no object is named, it is the object the surface shows.
|
|
100
|
+
*/
|
|
101
|
+
export const AFFORDANCE_GRANTS = {
|
|
102
|
+
/** A create form's fields (every `ObjectForm` layout) and its form-wide lock. */
|
|
103
|
+
createFormFields: { crud: 'create', grant: 'create', field: 'create' },
|
|
104
|
+
/** An edit form's fields (every `ObjectForm` layout) and its form-wide lock. */
|
|
105
|
+
editFormFields: { crud: 'edit', grant: 'update', field: 'write' },
|
|
106
|
+
/**
|
|
107
|
+
* The record page's Edit — the header CTA and the record body's in-place
|
|
108
|
+
* editing (the details body and the highlights strip), each of whose fields
|
|
109
|
+
* asks the update question (objectui#12103) — and the record overlay's
|
|
110
|
+
* inline edit, the same body in the overlay the calendar, kanban and gantt
|
|
111
|
+
* open (`RecordDetailPanel` in `@object-ui/plugin-detail`).
|
|
112
|
+
*/
|
|
113
|
+
recordEdit: { crud: 'edit', grant: 'update', field: 'write' },
|
|
114
|
+
/** The record page's Delete. */
|
|
115
|
+
recordDelete: { crud: 'delete', grant: 'delete' },
|
|
116
|
+
/**
|
|
117
|
+
* An object list's New: the console list page's toolbar button and phone
|
|
118
|
+
* "+", and the `object-view` node's toolbar button (`@object-ui/plugin-view`).
|
|
119
|
+
*/
|
|
120
|
+
listNew: { crud: 'create', grant: 'create' },
|
|
121
|
+
/**
|
|
122
|
+
* An object list's Import, and the import wizard's writable target fields —
|
|
123
|
+
* and the AI build panel's spreadsheet import (`ExcelImportBar` in
|
|
124
|
+
* `@object-ui/app-shell`), the wizard's second mount, asked of the object it
|
|
125
|
+
* imports into.
|
|
126
|
+
*/
|
|
127
|
+
listImport: { crud: 'import', grant: 'create', field: 'create' },
|
|
128
|
+
/** The import wizard's template download — its endpoint answers 403 without the create grant. */
|
|
129
|
+
importTemplate: { crud: null, grant: 'create' },
|
|
130
|
+
/** A list's inline (in-cell) editing, each of whose cells asks the update question (objectui#12103). */
|
|
131
|
+
listInlineEdit: { crud: 'edit', grant: 'update', field: 'write' },
|
|
132
|
+
/** A list's bulk Delete. */
|
|
133
|
+
listBulkDelete: { crud: 'delete', grant: 'delete' },
|
|
134
|
+
/**
|
|
135
|
+
* A grid row's Edit — and the writes that update the same rows in place: a
|
|
136
|
+
* grid's bulk field update (a `bulkActionDefs` `update` operation, with its
|
|
137
|
+
* undo and retry), and a gantt task's in-chart edits (a bar's drag, resize
|
|
138
|
+
* and progress, its title edited in place, and its dependency links).
|
|
139
|
+
*/
|
|
140
|
+
rowEdit: { crud: 'edit', grant: 'update' },
|
|
141
|
+
/** A grid row's Delete — and a gantt row's Delete. */
|
|
142
|
+
rowDelete: { crud: 'delete', grant: 'delete' },
|
|
143
|
+
/** A grid's inline add-record row. */
|
|
144
|
+
gridAddRow: { crud: 'create', grant: 'create' },
|
|
145
|
+
/**
|
|
146
|
+
* A related list's "+ New", asked of the CHILD object — and a line-items
|
|
147
|
+
* panel's or a master-detail form's add-a-line (its Add, entry row and
|
|
148
|
+
* Duplicate), which creates a child under the same parent.
|
|
149
|
+
*/
|
|
150
|
+
relatedNew: { crud: 'create', grant: 'create' },
|
|
151
|
+
/** A related list row's Edit, asked of the child object. */
|
|
152
|
+
relatedRowEdit: { crud: 'edit', grant: 'update' },
|
|
153
|
+
/**
|
|
154
|
+
* A related list row's Delete, asked of the child object — and a line-items
|
|
155
|
+
* panel's or a master-detail EDIT form's remove-a-line, which deletes that
|
|
156
|
+
* child on Save.
|
|
157
|
+
*/
|
|
158
|
+
relatedRowDelete: { crud: 'delete', grant: 'delete' },
|
|
159
|
+
/**
|
|
160
|
+
* Setup's Clone of a declared item (objectui#12982): a declared sharing
|
|
161
|
+
* rule's Clone creates an organization rule, asked of the object the
|
|
162
|
+
* environment authors such items as (`sys_sharing_rule`).
|
|
163
|
+
*/
|
|
164
|
+
declarationClone: { crud: 'create', grant: 'create' },
|
|
165
|
+
/** A lookup picker's "Create new", asked of the TARGET object. */
|
|
166
|
+
lookupCreateNew: { crud: 'create', grant: 'create' },
|
|
167
|
+
/** The record Attachments panel's Upload, asked of `sys_attachment` (storage route, not the data door). */
|
|
168
|
+
attachmentUpload: { crud: null, grant: 'create' },
|
|
169
|
+
/** The record Attachments panel's per-row delete, asked of `sys_attachment`. */
|
|
170
|
+
attachmentDelete: { crud: null, grant: 'delete' },
|
|
171
|
+
/**
|
|
172
|
+
* The record feed's comment composer and its reply composer, asked of
|
|
173
|
+
* `sys_comment`: each creates a comment row.
|
|
174
|
+
*/
|
|
175
|
+
feedComment: { crud: 'create', grant: 'create' },
|
|
176
|
+
/**
|
|
177
|
+
* The record feed's reaction control, adding the caller's reaction (its Add
|
|
178
|
+
* button, or an emoji the caller has not reacted with), asked of
|
|
179
|
+
* `sys_comment_reaction`: it creates the caller's own reaction row.
|
|
180
|
+
*/
|
|
181
|
+
feedReactionAdd: { crud: 'create', grant: 'create' },
|
|
182
|
+
/**
|
|
183
|
+
* The same control taking the caller's reaction back, asked of
|
|
184
|
+
* `sys_comment_reaction`: it deletes the caller's own reaction row.
|
|
185
|
+
*/
|
|
186
|
+
feedReactionRemove: { crud: 'delete', grant: 'delete' },
|
|
187
|
+
/**
|
|
188
|
+
* The same control on a deployment with no `sys_comment_reaction` (the
|
|
189
|
+
* reactions column store), asked of `sys_comment`: a click updates the
|
|
190
|
+
* comment's `reactions` column.
|
|
191
|
+
*/
|
|
192
|
+
feedReactionColumn: { crud: 'edit', grant: 'update' },
|
|
193
|
+
/**
|
|
194
|
+
* The record feed's Edit on a comment or reply the caller wrote, asked of
|
|
195
|
+
* `sys_comment`: it updates that comment row (objectui#12268). Whose comment
|
|
196
|
+
* it is decides only which rows offer it; the server's own rule decides the
|
|
197
|
+
* write.
|
|
198
|
+
*/
|
|
199
|
+
feedCommentEdit: { crud: 'edit', grant: 'update' },
|
|
200
|
+
/** The same rows' Delete, asked of `sys_comment`: it deletes that comment row. */
|
|
201
|
+
feedCommentDelete: { crud: 'delete', grant: 'delete' },
|
|
202
|
+
/** A calendar's quick-create: an empty-day click, or a time-range drag in the week / day grid. */
|
|
203
|
+
calendarQuickCreate: { crud: 'create', grant: 'create' },
|
|
204
|
+
/** A calendar's drag-to-reschedule: moving or resizing an event writes its date fields. */
|
|
205
|
+
calendarReschedule: { crud: 'edit', grant: 'update' },
|
|
206
|
+
/** A kanban's card move: a cross-column drop writes the record's `groupBy` field. */
|
|
207
|
+
kanbanCardMove: { crud: 'edit', grant: 'update' },
|
|
208
|
+
};
|
|
209
|
+
const PREDICATES = {
|
|
210
|
+
create: 'createPredicates',
|
|
211
|
+
import: 'importPredicates',
|
|
212
|
+
edit: 'editPredicates',
|
|
213
|
+
delete: 'deletePredicates',
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Resolve one affordance: its row's CRUD-affordance bit (managed-object policy
|
|
217
|
+
* ∧ the effective API operation set) AND the caller's grant.
|
|
218
|
+
*/
|
|
219
|
+
export function resolveAffordance(affordance, { objectSchema, objectName, perms } = {}) {
|
|
220
|
+
const row = AFFORDANCE_GRANTS[affordance];
|
|
221
|
+
let predicates;
|
|
222
|
+
if (row.crud !== null) {
|
|
223
|
+
const ops = objectName ? perms?.getObjectApiOperations?.(objectName) : undefined;
|
|
224
|
+
const crud = resolveEffectiveCrudAffordances(objectSchema, ops);
|
|
225
|
+
if (!crud[row.crud])
|
|
226
|
+
return { allowed: false };
|
|
227
|
+
predicates = crud[PREDICATES[row.crud]];
|
|
228
|
+
}
|
|
229
|
+
if (objectName && perms && !perms.can(objectName, row.grant))
|
|
230
|
+
return { allowed: false };
|
|
231
|
+
return predicates ? { allowed: true, predicates } : { allowed: true };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The field-level half of a field affordance: may the caller write `field`
|
|
235
|
+
* through it? Asks the resolver the row's question (`create` or `write`); with
|
|
236
|
+
* no provider mounted (`isLoaded` false) it asks nothing and answers `true`.
|
|
237
|
+
*/
|
|
238
|
+
export function resolveFieldAffordance(affordance, perms, objectName, field) {
|
|
239
|
+
if (!perms?.isLoaded)
|
|
240
|
+
return true;
|
|
241
|
+
return perms.checkField(objectName, field, AFFORDANCE_GRANTS[affordance].field);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The form row for a form's mode: a create form's fields ask the create
|
|
245
|
+
* question, every other form that writes asks the edit question, and a `view`
|
|
246
|
+
* form writes nothing (`undefined`).
|
|
247
|
+
*/
|
|
248
|
+
export function formFieldsAffordance(mode) {
|
|
249
|
+
if (mode === 'view')
|
|
250
|
+
return undefined;
|
|
251
|
+
return mode === 'create' ? 'createFormFields' : 'editFormFields';
|
|
252
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* chart-category-key — the ONE answer to "which result column carries the
|
|
10
|
+
* CATEGORY?" for an object-bound chart (objectui#8269).
|
|
11
|
+
*
|
|
12
|
+
* The mirror of `chart-measure-key` (objectui#8266) on the other axis. That
|
|
13
|
+
* card landed `chartMeasureKey` over the contract's `chartAggregateValueKey`;
|
|
14
|
+
* the contract publishes a CATEGORY sibling in the same module —
|
|
15
|
+
* `chartAggregateCategoryKey` — and nothing in this repository read it.
|
|
16
|
+
*
|
|
17
|
+
* ## The disagreement this replaces
|
|
18
|
+
*
|
|
19
|
+
* Both dashboard relays composed the category binding as a LITERAL FLOOR:
|
|
20
|
+
*
|
|
21
|
+
* const xAxisKey = options.xField || 'name';
|
|
22
|
+
*
|
|
23
|
+
* and handed it to the `object-chart` node without ever consulting the
|
|
24
|
+
* `aggregate` that decides it. An object-bound aggregate returns one row per
|
|
25
|
+
* group, keyed by the raw `groupBy` field, so a widget declaring
|
|
26
|
+
* `aggregate: { function: 'count', groupBy: 'status' }` and no `options.xField`
|
|
27
|
+
* bound `'name'` against rows keyed `'status'`.
|
|
28
|
+
*
|
|
29
|
+
* Unlike the measure half, this one is LOUD: `hasNoCategoryKey`
|
|
30
|
+
* (`plugin-charts/src/AdvancedChartImpl.tsx`, framework#4033) fires and the
|
|
31
|
+
* author reads "This chart cannot plot its category axis: no row has a `name`
|
|
32
|
+
* field." The diagnostic is wrong-CAUSE — it names a binding the author never
|
|
33
|
+
* wrote and never mentions the `groupBy` they did — so it points at the wrong
|
|
34
|
+
* layer. Being loud makes it a lesser harm than objectui#8266, not a
|
|
35
|
+
* non-defect: measured over the rows a fieldless count returns,
|
|
36
|
+
* `[{status:'open',count:2},{status:'paid',count:5}]`, an `xAxisKey` of
|
|
37
|
+
* `'name'` refuses under BOTH series bindings, i.e. objectui#8266's fix does
|
|
38
|
+
* not reach this authoring shape at all.
|
|
39
|
+
*
|
|
40
|
+
* ## Why the answer is delegated rather than restated
|
|
41
|
+
*
|
|
42
|
+
* Restating the rule here would make this file a second opinion of a question
|
|
43
|
+
* the contract already answers — the objectui#5042 / #7544 / #8193 / #8168
|
|
44
|
+
* drift shape that `chart-measure-key` exists to end. So the rule stays
|
|
45
|
+
* upstream in `@objectstack/spec/ui` and this is the seam objectui-side callers
|
|
46
|
+
* share.
|
|
47
|
+
*
|
|
48
|
+
* ## ⚠️ NOT the same function as `resolveChartCategoryField`
|
|
49
|
+
*
|
|
50
|
+
* `plugin-charts`' own `resolveChartCategoryField` (objectui#8168) reads
|
|
51
|
+
* `aggregate.groupBy` first too — which is exactly why `ObjectChart` does NOT
|
|
52
|
+
* refuse this shape at its own level, and then forwards `schema.xAxisKey`
|
|
53
|
+
* verbatim as the render binding anyway. But the two answer DIFFERENT
|
|
54
|
+
* questions and must not be collapsed:
|
|
55
|
+
*
|
|
56
|
+
* - `resolveChartCategoryField` answers "which FIELD is the category?" — its
|
|
57
|
+
* structured-`groupBy` leg returns `node.field`, because its two readers
|
|
58
|
+
* are the refusal (does the author name a category at all?) and the
|
|
59
|
+
* field-metadata probe that loads that field's option labels and colours.
|
|
60
|
+
* - this function answers "which COLUMN do the returned rows carry it
|
|
61
|
+
* under?" — `groupBy.alias ?? groupBy.field` per the contract, because an
|
|
62
|
+
* `alias` (admitted by `ChartGroupBySchema`) renames the projected column.
|
|
63
|
+
*
|
|
64
|
+
* They coincide whenever no alias is written, which is why the distinction is
|
|
65
|
+
* easy to miss; conflating them would either bind an axis to a column the rows
|
|
66
|
+
* do not carry, or probe field metadata for a field that does not exist.
|
|
67
|
+
*/
|
|
68
|
+
import { type ChartAggregateLike } from '@objectstack/spec/ui';
|
|
69
|
+
/**
|
|
70
|
+
* The result column a chart's category axis / x-axis binding must name.
|
|
71
|
+
*
|
|
72
|
+
* `fallback` is the caller's own floor, used ONLY when the contract has no
|
|
73
|
+
* answer — a chart that declares no `aggregate` at all (its rows are raw
|
|
74
|
+
* records or authored literals, where the author's `xField` is the right key),
|
|
75
|
+
* an UNGROUPED aggregate (one row, no category column), or a `groupBy` shape
|
|
76
|
+
* `ChartGroupBySchema` already rejects. It is never a second opinion about an
|
|
77
|
+
* aggregate the contract CAN answer for.
|
|
78
|
+
*
|
|
79
|
+
* @param aggregate the chart's inline aggregate, or `undefined`
|
|
80
|
+
* @param fallback the key to bind when the contract has no answer
|
|
81
|
+
*/
|
|
82
|
+
export declare function chartCategoryKey(aggregate: ChartAggregateLike | undefined, fallback: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
* chart-category-key — the ONE answer to "which result column carries the
|
|
10
|
+
* CATEGORY?" for an object-bound chart (objectui#8269).
|
|
11
|
+
*
|
|
12
|
+
* The mirror of `chart-measure-key` (objectui#8266) on the other axis. That
|
|
13
|
+
* card landed `chartMeasureKey` over the contract's `chartAggregateValueKey`;
|
|
14
|
+
* the contract publishes a CATEGORY sibling in the same module —
|
|
15
|
+
* `chartAggregateCategoryKey` — and nothing in this repository read it.
|
|
16
|
+
*
|
|
17
|
+
* ## The disagreement this replaces
|
|
18
|
+
*
|
|
19
|
+
* Both dashboard relays composed the category binding as a LITERAL FLOOR:
|
|
20
|
+
*
|
|
21
|
+
* const xAxisKey = options.xField || 'name';
|
|
22
|
+
*
|
|
23
|
+
* and handed it to the `object-chart` node without ever consulting the
|
|
24
|
+
* `aggregate` that decides it. An object-bound aggregate returns one row per
|
|
25
|
+
* group, keyed by the raw `groupBy` field, so a widget declaring
|
|
26
|
+
* `aggregate: { function: 'count', groupBy: 'status' }` and no `options.xField`
|
|
27
|
+
* bound `'name'` against rows keyed `'status'`.
|
|
28
|
+
*
|
|
29
|
+
* Unlike the measure half, this one is LOUD: `hasNoCategoryKey`
|
|
30
|
+
* (`plugin-charts/src/AdvancedChartImpl.tsx`, framework#4033) fires and the
|
|
31
|
+
* author reads "This chart cannot plot its category axis: no row has a `name`
|
|
32
|
+
* field." The diagnostic is wrong-CAUSE — it names a binding the author never
|
|
33
|
+
* wrote and never mentions the `groupBy` they did — so it points at the wrong
|
|
34
|
+
* layer. Being loud makes it a lesser harm than objectui#8266, not a
|
|
35
|
+
* non-defect: measured over the rows a fieldless count returns,
|
|
36
|
+
* `[{status:'open',count:2},{status:'paid',count:5}]`, an `xAxisKey` of
|
|
37
|
+
* `'name'` refuses under BOTH series bindings, i.e. objectui#8266's fix does
|
|
38
|
+
* not reach this authoring shape at all.
|
|
39
|
+
*
|
|
40
|
+
* ## Why the answer is delegated rather than restated
|
|
41
|
+
*
|
|
42
|
+
* Restating the rule here would make this file a second opinion of a question
|
|
43
|
+
* the contract already answers — the objectui#5042 / #7544 / #8193 / #8168
|
|
44
|
+
* drift shape that `chart-measure-key` exists to end. So the rule stays
|
|
45
|
+
* upstream in `@objectstack/spec/ui` and this is the seam objectui-side callers
|
|
46
|
+
* share.
|
|
47
|
+
*
|
|
48
|
+
* ## ⚠️ NOT the same function as `resolveChartCategoryField`
|
|
49
|
+
*
|
|
50
|
+
* `plugin-charts`' own `resolveChartCategoryField` (objectui#8168) reads
|
|
51
|
+
* `aggregate.groupBy` first too — which is exactly why `ObjectChart` does NOT
|
|
52
|
+
* refuse this shape at its own level, and then forwards `schema.xAxisKey`
|
|
53
|
+
* verbatim as the render binding anyway. But the two answer DIFFERENT
|
|
54
|
+
* questions and must not be collapsed:
|
|
55
|
+
*
|
|
56
|
+
* - `resolveChartCategoryField` answers "which FIELD is the category?" — its
|
|
57
|
+
* structured-`groupBy` leg returns `node.field`, because its two readers
|
|
58
|
+
* are the refusal (does the author name a category at all?) and the
|
|
59
|
+
* field-metadata probe that loads that field's option labels and colours.
|
|
60
|
+
* - this function answers "which COLUMN do the returned rows carry it
|
|
61
|
+
* under?" — `groupBy.alias ?? groupBy.field` per the contract, because an
|
|
62
|
+
* `alias` (admitted by `ChartGroupBySchema`) renames the projected column.
|
|
63
|
+
*
|
|
64
|
+
* They coincide whenever no alias is written, which is why the distinction is
|
|
65
|
+
* easy to miss; conflating them would either bind an axis to a column the rows
|
|
66
|
+
* do not carry, or probe field metadata for a field that does not exist.
|
|
67
|
+
*/
|
|
68
|
+
import { chartAggregateCategoryKey } from '@objectstack/spec/ui';
|
|
69
|
+
/**
|
|
70
|
+
* The result column a chart's category axis / x-axis binding must name.
|
|
71
|
+
*
|
|
72
|
+
* `fallback` is the caller's own floor, used ONLY when the contract has no
|
|
73
|
+
* answer — a chart that declares no `aggregate` at all (its rows are raw
|
|
74
|
+
* records or authored literals, where the author's `xField` is the right key),
|
|
75
|
+
* an UNGROUPED aggregate (one row, no category column), or a `groupBy` shape
|
|
76
|
+
* `ChartGroupBySchema` already rejects. It is never a second opinion about an
|
|
77
|
+
* aggregate the contract CAN answer for.
|
|
78
|
+
*
|
|
79
|
+
* @param aggregate the chart's inline aggregate, or `undefined`
|
|
80
|
+
* @param fallback the key to bind when the contract has no answer
|
|
81
|
+
*/
|
|
82
|
+
export function chartCategoryKey(aggregate, fallback) {
|
|
83
|
+
return chartAggregateCategoryKey(aggregate) ?? fallback;
|
|
84
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* chart-measure-key — the ONE answer to "which result column carries the
|
|
10
|
+
* measure?" for an object-bound chart (objectui#8266).
|
|
11
|
+
*
|
|
12
|
+
* ## The disagreement this replaces
|
|
13
|
+
*
|
|
14
|
+
* The question was answered independently in three places, and for a FIELDLESS
|
|
15
|
+
* `count` — the normal way to author "how many records per status", and the
|
|
16
|
+
* most common dashboard chart there is — two of them answered differently:
|
|
17
|
+
*
|
|
18
|
+
* - the ROW PROJECTION (`aggregateValueKey` / `aggregateRecords` /
|
|
19
|
+
* `runAggregate`'s alias, all in `plugin-charts/src/ObjectChart.tsx`) keys
|
|
20
|
+
* the value column `'count'`, the alias the engine projects `COUNT(*)`
|
|
21
|
+
* under;
|
|
22
|
+
* - the SERIES BINDING, spelled twice in `plugin-dashboard`
|
|
23
|
+
* (`DashboardGridLayout.tsx` and `DashboardRenderer.tsx`), read
|
|
24
|
+
* `aggregate.field || (options.yField || 'value')` and so bound `'value'`.
|
|
25
|
+
*
|
|
26
|
+
* A `dataKey` naming a column no row carries plots nothing, and nothing says
|
|
27
|
+
* so: measured on `origin/main` `0fa7a9c83`, rows `[{status:'open',count:2},
|
|
28
|
+
* {status:'paid',count:5}]` with `series:[{dataKey:'value'}]` rendered a
|
|
29
|
+
* `.recharts-surface` with the category ticks "open"/"paid", **0 bars, 0
|
|
30
|
+
* rectangles**, no refusal and no empty state — the same rows under
|
|
31
|
+
* `dataKey:'count'` drew 2 rectangles. An empty chart reads as "no data yet",
|
|
32
|
+
* which is exactly what an author with a genuinely empty object also sees.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the answer is delegated rather than restated
|
|
35
|
+
*
|
|
36
|
+
* `chartAggregateValueKey` in `@objectstack/spec/ui` is the CONTRACT's own
|
|
37
|
+
* derivation — "the VALUE column an object-bound aggregate produces … what a
|
|
38
|
+
* chart's series / y-axis binding must name". Producers and checkers already
|
|
39
|
+
* read it. Restating the rule here would make this file a fourth opinion of the
|
|
40
|
+
* same question, which is the shape (objectui#5042 / #7544 / #8193 / #8168)
|
|
41
|
+
* this module exists to end. So the rule lives upstream in the spec and this is
|
|
42
|
+
* the seam objectui-side callers share, exactly as `humanizeLabel` (objectui#5444)
|
|
43
|
+
* and `buildChartSeries` (ADR-0021) already are.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ The inverse fix — making the row projection key a fieldless count
|
|
46
|
+
* `'value'` — is not available: it contradicts a pin the tree already carries
|
|
47
|
+
* (`ObjectChart.aggregateResultColumns.test.ts`) and renames the column every
|
|
48
|
+
* other consumer reads, including the engine's own `COUNT(*)` alias.
|
|
49
|
+
*/
|
|
50
|
+
import { type ChartAggregateLike } from '@objectstack/spec/ui';
|
|
51
|
+
export type { ChartAggregateLike };
|
|
52
|
+
/**
|
|
53
|
+
* The result column a chart's series must bind to.
|
|
54
|
+
*
|
|
55
|
+
* `fallback` is the caller's own floor, used ONLY when the contract has no
|
|
56
|
+
* answer — a chart that declares no `aggregate` at all (its rows are raw
|
|
57
|
+
* records or authored literals, where the author's `yField` is the right key),
|
|
58
|
+
* or an aggregate shape `ChartAggregateSchema` already rejects. It is never a
|
|
59
|
+
* second opinion about an aggregate the contract CAN answer for.
|
|
60
|
+
*
|
|
61
|
+
* @param aggregate the chart's inline aggregate, or `undefined`
|
|
62
|
+
* @param fallback the key to bind when the contract has no answer
|
|
63
|
+
*/
|
|
64
|
+
export declare function chartMeasureKey(aggregate: ChartAggregateLike | undefined, fallback: string): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
* chart-measure-key — the ONE answer to "which result column carries the
|
|
10
|
+
* measure?" for an object-bound chart (objectui#8266).
|
|
11
|
+
*
|
|
12
|
+
* ## The disagreement this replaces
|
|
13
|
+
*
|
|
14
|
+
* The question was answered independently in three places, and for a FIELDLESS
|
|
15
|
+
* `count` — the normal way to author "how many records per status", and the
|
|
16
|
+
* most common dashboard chart there is — two of them answered differently:
|
|
17
|
+
*
|
|
18
|
+
* - the ROW PROJECTION (`aggregateValueKey` / `aggregateRecords` /
|
|
19
|
+
* `runAggregate`'s alias, all in `plugin-charts/src/ObjectChart.tsx`) keys
|
|
20
|
+
* the value column `'count'`, the alias the engine projects `COUNT(*)`
|
|
21
|
+
* under;
|
|
22
|
+
* - the SERIES BINDING, spelled twice in `plugin-dashboard`
|
|
23
|
+
* (`DashboardGridLayout.tsx` and `DashboardRenderer.tsx`), read
|
|
24
|
+
* `aggregate.field || (options.yField || 'value')` and so bound `'value'`.
|
|
25
|
+
*
|
|
26
|
+
* A `dataKey` naming a column no row carries plots nothing, and nothing says
|
|
27
|
+
* so: measured on `origin/main` `0fa7a9c83`, rows `[{status:'open',count:2},
|
|
28
|
+
* {status:'paid',count:5}]` with `series:[{dataKey:'value'}]` rendered a
|
|
29
|
+
* `.recharts-surface` with the category ticks "open"/"paid", **0 bars, 0
|
|
30
|
+
* rectangles**, no refusal and no empty state — the same rows under
|
|
31
|
+
* `dataKey:'count'` drew 2 rectangles. An empty chart reads as "no data yet",
|
|
32
|
+
* which is exactly what an author with a genuinely empty object also sees.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the answer is delegated rather than restated
|
|
35
|
+
*
|
|
36
|
+
* `chartAggregateValueKey` in `@objectstack/spec/ui` is the CONTRACT's own
|
|
37
|
+
* derivation — "the VALUE column an object-bound aggregate produces … what a
|
|
38
|
+
* chart's series / y-axis binding must name". Producers and checkers already
|
|
39
|
+
* read it. Restating the rule here would make this file a fourth opinion of the
|
|
40
|
+
* same question, which is the shape (objectui#5042 / #7544 / #8193 / #8168)
|
|
41
|
+
* this module exists to end. So the rule lives upstream in the spec and this is
|
|
42
|
+
* the seam objectui-side callers share, exactly as `humanizeLabel` (objectui#5444)
|
|
43
|
+
* and `buildChartSeries` (ADR-0021) already are.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ The inverse fix — making the row projection key a fieldless count
|
|
46
|
+
* `'value'` — is not available: it contradicts a pin the tree already carries
|
|
47
|
+
* (`ObjectChart.aggregateResultColumns.test.ts`) and renames the column every
|
|
48
|
+
* other consumer reads, including the engine's own `COUNT(*)` alias.
|
|
49
|
+
*/
|
|
50
|
+
import { chartAggregateValueKey } from '@objectstack/spec/ui';
|
|
51
|
+
/**
|
|
52
|
+
* The result column a chart's series must bind to.
|
|
53
|
+
*
|
|
54
|
+
* `fallback` is the caller's own floor, used ONLY when the contract has no
|
|
55
|
+
* answer — a chart that declares no `aggregate` at all (its rows are raw
|
|
56
|
+
* records or authored literals, where the author's `yField` is the right key),
|
|
57
|
+
* or an aggregate shape `ChartAggregateSchema` already rejects. It is never a
|
|
58
|
+
* second opinion about an aggregate the contract CAN answer for.
|
|
59
|
+
*
|
|
60
|
+
* @param aggregate the chart's inline aggregate, or `undefined`
|
|
61
|
+
* @param fallback the key to bind when the contract has no answer
|
|
62
|
+
*/
|
|
63
|
+
export function chartMeasureKey(aggregate, fallback) {
|
|
64
|
+
return chartAggregateValueKey(aggregate) ?? fallback;
|
|
65
|
+
}
|