diff --git a/.changeset/12082-census-leg-a.md b/.changeset/12082-census-leg-a.md new file mode 100644 index 0000000000..460905623a --- /dev/null +++ b/.changeset/12082-census-leg-a.md @@ -0,0 +1,39 @@ +--- +'@object-ui/core': patch +'@object-ui/plugin-detail': patch +'@object-ui/plugin-gantt': patch +'@object-ui/plugin-grid': patch +'@object-ui/plugin-form': patch +'@object-ui/app-shell': patch +--- + +More write affordances read the affordance-to-grant map, so a caller without the +grant is no longer offered a write the server then refuses (objectui#12082, leg A +of the write-census backlog). No row is added: each reuses the row of the same +verb on the same object. + +- **The record overlay's inline edit** — the overlay the calendar, the kanban and + the gantt open on a record (`RecordDetailPanel`) — reads the `recordEdit` row, + the row the record page reads for the same in-place edit: the object's policy, + the effective API operation set and the caller's update grant. A closed row + opens no inline-edit session, as an absent `onFieldSave` already did. Each + field still asks its own field question, as since objectui#12103. +- **The gantt's in-chart writes** read `rowEdit` (a bar's drag, resize and + progress, its title edited in place, and its dependency links) and `rowDelete` + (a row's Delete). A closed row withholds the handler the chart draws the + affordance from, the way `readOnly` and `lockField` already do. +- **A grid's bulk field update** (a `bulkActionDefs` entry with `operation: + 'update'`) reads `rowEdit`, as its bulk delete reads `rowDelete`: a closed row + removes the button from the selection bar, and with it the dialog, its undo + and its retry. `custom` operations are unchanged. +- **A master-detail form's line grids** read `relatedNew` (Add, the entry row + and Duplicate) and `relatedRowDelete` (Remove) on the CHILD object, as a + line-items panel does. Remove asks the delete grant on an edit form only: a + create form's lines are drafts, and a removed one is never a server delete. +- **A line-items panel** now hands its child object's schema to those two rows, + so the child's own policy (`managedBy`, `userActions`) is read too. +- **The AI build panel's spreadsheet import** (`ExcelImportBar`) reads the + `listImport` row of the object it imports into, as the list page's Import + does: without it, Import is disabled for that object. + +With no permission provider mounted every one of these reads open, as before. diff --git a/content/docs/plugins/plugin-form.mdx b/content/docs/plugins/plugin-form.mdx index f181de1fce..06a605e19b 100644 --- a/content/docs/plugins/plugin-form.mdx +++ b/content/docs/plugins/plugin-form.mdx @@ -474,7 +474,14 @@ Duplicate) only when the caller may create the CHILD object, and removing one only when they may delete it — the `relatedNew` and `relatedRowDelete` rows of the affordance-to-grant map in `@object-ui/core`, the same rows a related list reads for the same two writes. Its cells ask each column's field question, as the form -layouts do. With no permission provider mounted both read open. +layouts do. The child's own policy is read too: the panel hands the child +object's schema to those two rows, so a child whose `userActions` close create or +delete offers neither. With no permission provider mounted both read open. + +A `MasterDetailForm`'s line grids read the same two rows on the child object. +Remove asks the delete grant on an edit form only: a create form saves the parent +and one create per line, so a line removed before that first save is a draft, +never a server delete, and stays removable for a caller who may only create. ## Examples diff --git a/content/docs/plugins/plugin-gantt.mdx b/content/docs/plugins/plugin-gantt.mdx index eac58762c7..6d9653ee83 100644 --- a/content/docs/plugins/plugin-gantt.mdx +++ b/content/docs/plugins/plugin-gantt.mdx @@ -36,6 +36,14 @@ The `@object-ui/plugin-gantt` plugin provides Gantt chart visualization for Obje `dataSource.update()` automatically (optimistic local update + revert on failure). See the [package README](https://github.com/objectstack-ai/objectui/blob/main/packages/plugin-gantt/README.md#drag-and-drop-rescheduling) for details on the lower-level `onTaskUpdate` hook when embedding `` directly. +- **Writes follow the caller's grant**: a bar's drag, resize and progress, its + title edited in place and its dependency links are offered only when the + `rowEdit` row of the affordance-to-grant map (`@object-ui/core`) allows them — + the object's policy, the effective API operation set and the caller's + **update** grant — and a row's Delete only when `rowDelete` does. The record + drawer's inline edit reads the record page's `recordEdit` row. With no + permission provider mounted they read open, as `readOnly` and `lockField` + still close them. diff --git a/content/docs/plugins/plugin-grid.mdx b/content/docs/plugins/plugin-grid.mdx index cbbda04ec5..16cc4268c2 100644 --- a/content/docs/plugins/plugin-grid.mdx +++ b/content/docs/plugins/plugin-grid.mdx @@ -608,6 +608,15 @@ target from that field, so a lookup param renders the record picker and the confirm step shows the picked record's label. A param whose field is not in the object metadata is refused, and the run is blocked. +The selection bar offers a write only to a caller who may make it. A bulk Delete +is the `rowDelete` row of the affordance-to-grant map (`@object-ui/core`), and a +`bulkActionDefs` entry with `operation: 'update'` — a patch written to every +selected record — is the `rowEdit` row, the same rows the row menu's Delete and +Edit read: the object's policy, the effective API operation set and the +caller's **delete** or **update** grant. A closed row removes the button, and +with it the dialog, its undo and its retry. `custom` entries are unchanged. +With no permission provider mounted both read open. + ### Row callbacks are component props `onRowClick`, `onRowSelect`, `onCellChange`, `onRowSave`, `onBatchSave`, diff --git a/packages/app-shell/src/console/ai/ExcelImportBar.tsx b/packages/app-shell/src/console/ai/ExcelImportBar.tsx index fa2cdf4b71..94d105b87d 100644 --- a/packages/app-shell/src/console/ai/ExcelImportBar.tsx +++ b/packages/app-shell/src/console/ai/ExcelImportBar.tsx @@ -12,6 +12,11 @@ * mapping, dry-run and the real server-side import. So the magic-flow trial * goes from "one sentence → app with demo data" to "app with MY data" — the * retention hinge for a spreadsheet refugee. + * + * Import is the `listImport` row of the affordance-to-grant map (objectui#12082), + * the row the console list's Import reads for the same wizard: it is offered for + * the picked object only when the caller's create grant and the server's + * effective operation set allow it, and picking another object asks again. */ import { useEffect, useMemo, useState } from 'react'; @@ -27,6 +32,8 @@ import { import { createAuthenticatedFetch } from '@object-ui/auth'; import { toast } from 'sonner'; import { useObjectTranslation } from '@object-ui/i18n'; +import { resolveAffordance } from '@object-ui/core'; +import { usePermissions } from '@object-ui/permissions'; import { ImportWizard } from '@object-ui/plugin-grid'; interface WizardField { @@ -133,6 +140,17 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: const [loadingFields, setLoadingFields] = useState(false); const [fields, setFields] = useState(null); const [open, setOpen] = useState(false); + const perms = usePermissions(); + // objectui#12082 — the `listImport` row, asked of the picked object. The bar + // has not read that object's schema when it offers Import (it reads it on the + // press), so the policy half reads the default bucket; the grant and the + // effective operation set are the server's own refusal. With no permission + // provider mounted both read open, as before. + const importGranted = resolveAffordance('listImport', { + objectSchema: null, + objectName: selected, + perms, + }).allowed; // List the env's user objects so the user can target the import. Uses the // uncached meta list endpoint; filters platform/system objects. @@ -162,7 +180,7 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: }, [authFetch, defaultObjectName]); // eslint-disable-line react-hooks/exhaustive-deps async function openWizard() { - if (!selected) return; + if (!selected || !importGranted) return; setLoadingFields(true); try { const schema = await dataSource.getObjectSchema(selected); @@ -215,7 +233,7 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: onPick={setSelected} disabled={!objects || objects.length === 0} /> -