Skip to content

Commit ad2c286

Browse files
fix(plugin-grid): inline cells read the record-grained update verdict the row kebab reads (#27384)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ88z34ttNLNf2N8f3PHSjhj7)_ Fixes #27365 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:列表行内编辑现在按「这一行」的服务端写权限判定;服务端拒绝的行点不开单元格编辑器,和该行的 ⋮ 菜单给出同一个答案。 - 为什么改:对象级有编辑权、但共享/归属/RLS 拒绝某行时,⋮ 菜单已隐藏「编辑」,单元格却仍可编辑,保存时被服务端整单拒绝。 - 风险与回滚:只收窄不放宽;没拿到行级答案(未返回、请求失败、行无 id)时保持原行为。回滚即 revert 本 PR。 - 先看:`ui/packages/plugin-grid/src/ObjectGrid.tsx`(`isRowEditable` 与 `update: canEdit || inlineEditable`)、`ui/packages/components/src/renderers/complex/data-table.tsx`(`rowMayEdit`)、`ui/packages/types/src/data-display.ts`(新运行时槽位)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-grid/src/__tests__/inlineEditRecordVerdict-27365.test.tsx`,预期 8 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes An inline-editable grid gated its cells on the object-level affordance only (`inlineEditable`), while the row kebab on the same rows also read the server's record-grained `update` verdict (`useRecordCrudVerdicts`). A row refused by sharing, ownership or RLS lost Edit from its kebab but still opened a cell editor, and the save was refused whole. Now the cells read the same verdict. - `@object-ui/types`: `DataTableSchema` declares a new runtime slot, `isRowEditable?: (row: any) => boolean`. The zod mirror refuses it by name (`handlerKeyRefusal`, `runtime-slot`), like `renderCellEditor` and `rowGroups`. The ledgers move with it: the `KnownDrift` entry for `data-display.zod.ts#DataTableSchema` gains the key (51 entries, 88 keys), and the read lists in the `body`/`children` tombstones name it. - `@object-ui/components`: `data-table` reads `isRowEditable` through one `rowMayEdit(row)` check in `startEdit` (the door click, double-click and Enter all pass through), in the Enter key path, and in the per-cell `isEditable`. A refused row reads like a row of a read-only table: no editor, no edit cursor, and a click reaches the row's own click handler. With the slot absent, every row behaves as before. - `@object-ui/plugin-grid`: `ObjectGrid` supplies `isRowEditable` as `resolveRowRecordCrudAffordance(inlineEditable, recordVerdict(rowRecordId(row), 'update'))`, the exact accessor the kebab reads. It asks the batched probe whenever the grid is inline-editable (`update: canEdit || inlineEditable`), so an editable grid whose kebab offers no Edit is narrowed too. That is still one probe, read by both affordances, with no second evaluator (ruling 6457467702 B). - ListView reaches the cells through `ObjectGrid` (`type: 'object-grid'`, `editable: inlineEdit && inlineEditOffered`), so it has no separate door and needs no plugin-list change. - Test doubles: editable-grid suites now get the explain probe. Nine plugin-grid suites and one app-shell suite (`ListView.gridRowColorSelect-12393`) answer it from an explain double instead of reaching the network. This is test-only, per claim amendment 6487558539. The assertions are unchanged. ## Why The card: one grid gave two answers for one row. Fail-open is preserved exactly as the `useRecordCrudVerdicts` docblock requires: an unknown verdict keeps the object-level behaviour. ## Tests - `plugin-grid/src/__tests__/inlineEditRecordVerdict-27365.test.tsx` (8): a refused row opens no editor on click or Enter and shows no edit cursor. CONTROLs: an allowed row stays editable; a non-OK response, a network failure, a response with no `records[]`, and the pre-answer window all keep the row editable. The kebab and the cells agree on both rows from ONE `update` probe. An editable grid with no kebab Edit still asks and still narrows. - `components/src/__tests__/data-table-row-editable-27365.test.tsx` (7): the seam on its own covers click, double-click and Enter, a click that reaches `onRowClick`, an allowed-row control, the no-predicate control, and narrows-only. - `types/src/__tests__/data-table-declared-keys-6882.test.ts`: the declared exact shape, a widened-return control (`@ts-expect-error`), and the zod mirror refusing an authored `isRowEditable` by name. ## Acceptance notes - Ablation 1, ObjectGrid's per-row read replaced by `(_row: any) => true` via `scripts/ablation-replace.mjs`: 4 of 8 tests in the new grid suite red (the repro, agreement, no-kebab and pre-answer legs). Restored blob equals HEAD and `git diff HEAD` is empty. - Ablation 2, data-table's `rowMayEdit` replaced by `() => true`: 3 of 7 tests in the seam suite red (click, double-click, Enter). Restored blob equals HEAD. - `ui/apps/site` was retired on main. After merging main, an ignored leftover directory (node_modules) in the worktree reddened `quick-reference-commands-4149` locally. That was a local-state artifact only: the test is green once the leftover is removed, and nothing in the tree changed. ## Verification All readings were taken on head `3b46f5a1` (origin/main `c4c515ad` merged in), with the `@object-ui/console^...` closure built under the lock. <details><summary>Tests</summary> - `pnpm --dir ui exec vitest run` over the two new suites, `packages/types/`, the ten double-carrying suites, `inlineEditPermissionGate` and `rowRecordCrudVerdict`: Test Files 398 passed (398), Tests 10521 passed (10521). - Before the merge, at `e77abe74` plus the doubles: `packages/plugin-grid/`, `packages/types/` and the data-table suites went 934/935 files green with one skipped; the 9 red files were the network-escape suites that now carry the double (46/46). Across 59 ui consumer suites that render an editable grid, 58 were green and the app-shell one was fixed (2/2). - `pnpm --dir ui exec vitest run scripts/`: 168 passed, 2 skipped, 1 red on a stale ignored `ui/apps/site` leftover. That test is green (10/10) once the leftover is removed. - `pnpm --filter @object-ui/types|components|plugin-grid run type-check`: exit 0, all three. </details> <details><summary>Gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands`: 35 families, all run on `3b46f5a1`, every exit code 0, including `node scripts/check-sdui-manifest.mjs`. - `dispatch-gates --ran`: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. - `pnpm --dir ui run check:new-line-citations`: 0 new citations. - Repo-wide lint and the full ui battery are CI's (`Lint UI`, `Type Check UI`, `Test UI`). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` was not run locally (CI `Lint UI` and `Lint & Repo Gates` own it). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a, not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch's required Chinese summary). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxba6qJDidRrN2u71o5N)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e200b4 commit ad2c286

19 files changed

Lines changed: 655 additions & 28 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
'@object-ui/components': patch
4+
'@object-ui/types': patch
5+
---
6+
7+
fix(plugin-grid): inline edit opens no cell editor on a row the server's record-grained write verdict refuses
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
An editable grid gated its cells on the object-level grant only, while the row kebab on the same rows also read the server's record-grained `update` verdict. A row that sharing, ownership or RLS refuses lost Edit from its kebab but still opened a cell editor, and the save was refused by the server. Now the cells read the same verdict as the kebab:
13+
14+
- **A refused row reads as read-only.** No click, double-click or Enter opens an editor on it, and its cells show no edit cursor. A click on such a cell reaches the row as it does on a read-only grid. Selection, navigation and the row menu are unchanged.
15+
- **One verdict, fail open.** The cells and the kebab read one batched explain probe for the rows on screen. A row with no answer yet, a failed or absent probe, or a row with no id keeps the object-level behaviour, as before. An editable grid with no kebab Edit now asks the same probe.
16+
- **New runtime slot `DataTableSchema.isRowEditable`.** `(row) => boolean`, supplied by the host and read by `data-table` before it opens a cell editor. It only narrows `editable`: absent, every row follows `editable` and the column locks as before. It is a function, not metadata, so the zod mirror refuses an authored value by name.

‎ui/packages/app-shell/src/__tests__/ListView.gridRowColorSelect-12393.test.tsx‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,29 @@ afterAll(() => {
209209
beforeEach(() => {
210210
vi.clearAllMocks();
211211
lastGridProps = null;
212+
// objectstack#27365 — an inline-editable grid asks the record-grained
213+
// `update` verdict for its rows (`POST /api/v1/security/explain`). Answer it
214+
// here, every row permitted, so it never reaches the network; any other URL
215+
// is refused loudly rather than answered.
216+
vi.stubGlobal('fetch', vi.fn(async (url: unknown, init?: { body?: unknown }) => {
217+
if (!String(url).endsWith('/api/v1/security/explain')) {
218+
throw new Error(`unexpected fetch in this suite: ${String(url)}`);
219+
}
220+
const body = JSON.parse(String(init?.body ?? '{}')) as { recordIds?: string[] };
221+
return {
222+
ok: true,
223+
status: 200,
224+
json: async () => ({
225+
allowed: true,
226+
records: (body.recordIds ?? []).map((recordId) => ({ recordId, visible: true })),
227+
}),
228+
};
229+
}));
230+
});
231+
afterEach(() => {
232+
cleanup();
233+
vi.unstubAllGlobals();
212234
});
213-
afterEach(() => cleanup());
214235

215236
const settle = () => act(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
216237

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
/**
10+
* objectstack#27365 — `DataTableSchema.isRowEditable`, the host's per-row
11+
* inline-edit verdict.
12+
*
13+
* `editable` and the per-column locks answer for every row alike. A host that
14+
* knows one row is refused by the server (ObjectGrid, from the record-grained
15+
* write verdict) hands the table a per-row predicate; a row it answers `false`
16+
* for must read exactly like a row of a read-only table:
17+
*
18+
* - no trigger opens an editor on it — click, double-click, Enter;
19+
* - its cells take no edit cursor, and a click on them is NOT swallowed, so
20+
* the row's own click (ObjectGrid's record drawer) still fires;
21+
* - every other row is untouched, and with no predicate every row is
22+
* editable exactly as before.
23+
*/
24+
import { describe, it, expect, vi } from 'vitest';
25+
import { fireEvent } from '@testing-library/react';
26+
import '@testing-library/jest-dom';
27+
import { renderComponent } from './test-utils';
28+
// Registers the renderers at module scope, NOT inside a `beforeAll` (objectui#3010).
29+
import '../renderers';
30+
31+
const ROWS = [
32+
{ id: 'own', name: 'My Project' },
33+
{ id: 'other', name: 'Legacy Sunset' },
34+
];
35+
36+
const baseSchema = (extra: Record<string, unknown> = {}) =>
37+
({
38+
type: 'data-table' as const,
39+
editable: true,
40+
columns: [{ header: 'Name', accessorKey: 'name' }],
41+
data: ROWS,
42+
...extra,
43+
}) as any;
44+
45+
/** The `<td>` showing `text`. */
46+
const cellOf = (container: HTMLElement, text: string) =>
47+
Array.from(container.querySelectorAll('tbody td')).find(
48+
(td) => td.textContent?.trim() === text,
49+
) as HTMLElement;
50+
51+
const refuseOther = (row: { id: string }) => row.id !== 'other';
52+
53+
describe('data-table — isRowEditable narrows editing per row (objectstack#27365)', () => {
54+
it('a refused row opens no editor on click, and its click reaches the row', () => {
55+
const onRowClick = vi.fn();
56+
const { container } = renderComponent(
57+
baseSchema({ singleClickEdit: true, isRowEditable: refuseOther, onRowClick }),
58+
);
59+
const cell = cellOf(container, 'Legacy Sunset');
60+
expect(cell).not.toHaveClass('cursor-text');
61+
fireEvent.click(cell);
62+
expect(cell.querySelector('input')).toBeNull();
63+
expect(onRowClick).toHaveBeenCalledTimes(1);
64+
});
65+
66+
it('a refused row opens no editor on double-click', () => {
67+
const { container } = renderComponent(baseSchema({ isRowEditable: refuseOther }));
68+
const cell = cellOf(container, 'Legacy Sunset');
69+
fireEvent.doubleClick(cell);
70+
expect(cell.querySelector('input')).toBeNull();
71+
});
72+
73+
it('a refused row opens no editor on Enter', () => {
74+
const { container } = renderComponent(baseSchema({ isRowEditable: refuseOther }));
75+
const cell = cellOf(container, 'Legacy Sunset');
76+
fireEvent.keyDown(cell, { key: 'Enter' });
77+
expect(cell.querySelector('input')).toBeNull();
78+
});
79+
80+
it('CONTROL: the allowed row of the same table still opens its editor, on each trigger', () => {
81+
const { container } = renderComponent(
82+
baseSchema({ singleClickEdit: true, isRowEditable: refuseOther }),
83+
);
84+
const cell = cellOf(container, 'My Project');
85+
expect(cell).toHaveClass('cursor-text');
86+
fireEvent.click(cell);
87+
expect(cell.querySelector('input')).not.toBeNull();
88+
});
89+
90+
it('CONTROL: Enter on the allowed row opens its editor', () => {
91+
const { container } = renderComponent(baseSchema({ isRowEditable: refuseOther }));
92+
const cell = cellOf(container, 'My Project');
93+
fireEvent.keyDown(cell, { key: 'Enter' });
94+
expect(cell.querySelector('input')).not.toBeNull();
95+
});
96+
97+
it('CONTROL: with no predicate every row is editable, as before', () => {
98+
const { container } = renderComponent(baseSchema({ singleClickEdit: true }));
99+
const cell = cellOf(container, 'Legacy Sunset');
100+
fireEvent.click(cell);
101+
expect(cell.querySelector('input')).not.toBeNull();
102+
});
103+
104+
it('narrows only: a predicate answering `true` cannot open a table that is not `editable`', () => {
105+
const { container } = renderComponent(
106+
baseSchema({ editable: false, singleClickEdit: true, isRowEditable: () => true }),
107+
);
108+
const cell = cellOf(container, 'My Project');
109+
fireEvent.click(cell);
110+
expect(cell.querySelector('input')).toBeNull();
111+
});
112+
});

‎ui/packages/components/src/renderers/complex/data-table.tsx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
12061206
borderless = false,
12071207
disableInnerScroll = false,
12081208
rowGroups,
1209+
isRowEditable,
12091210
// Read ONLY to diagnose it. `data-table` does not resolve `bind` and the
12101211
// objectui#6575 ruling is explicit that it must not start — see
12111212
// `dataTableBindDiagnostic.ts`.
@@ -2262,9 +2263,14 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
22622263
setDragOverColumn(null);
22632264
};
22642265

2266+
// objectstack#27365 — the host's per-row verdict narrows `editable` for one
2267+
// row. Absent, or answering anything but `false`, the row follows
2268+
// `editable`; a refused row reads exactly like a row of a read-only table.
2269+
const rowMayEdit = (row: any): boolean => isRowEditable?.(row) !== false;
2270+
22652271
// Cell editing handlers
22662272
const startEdit = (row: any, columnKey: string) => {
2267-
if (!editable) return;
2273+
if (!editable || !rowMayEdit(row)) return;
22682274
const rowKey = pendingRowKeyOf(row);
22692275

22702276
// Already editing THIS cell — do nothing. Re-entering would reset `editValue`
@@ -2798,7 +2804,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
27982804
return;
27992805
}
28002806

2801-
if (!editable) return;
2807+
if (!editable || !rowMayEdit(paginatedData[rowIndex])) return;
28022808

28032809
const column = columns.find(col => col.accessorKey === columnKey);
28042810
if (column?.editable === false) return;
@@ -3654,8 +3660,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
36543660
// A masked column reads as NOT editable here too
36553661
// (objectui#10583), so its cell neither shows the
36563662
// edit cursor nor swallows the row's click —
3657-
// `startEdit` would refuse it anyway.
3658-
const isEditable = editable && col.editable !== false && !col.masked;
3663+
// `startEdit` would refuse it anyway. Nor is a row
3664+
// the host's per-row verdict refuses (objectstack#27365).
3665+
const isEditable = editable && col.editable !== false && !col.masked && rowMayEdit(row);
36593666
const isFrozen = frozenColumns > 0 && colIndex < frozenColumns;
36603667
const frozenOffset = isFrozen
36613668
? measuredStickyLefts?.[leadingCellCount + colIndex]

‎ui/packages/plugin-grid/src/ObjectGrid.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import {
7070
import { ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Download, Rows2, Rows3, Rows4, AlignJustify, Loader2 } from 'lucide-react';
7171
import { useRowColor } from './useRowColor';
7272
import { GroupRow } from './GroupRow';
73-
import { resolveRowCrudAffordances } from './rowCrudAffordances';
73+
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
7474
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
7575
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
7676
import { resolveBulkActions, resolvePromotedBulkParams } from './resolveBulkActions';
@@ -1007,10 +1007,13 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
10071007
)),
10081008
[data],
10091009
);
1010+
// objectstack#27365 — the inline cells ask the same `update` question about
1011+
// the same rows, so an editable grid whose kebab offers no Edit still asks
1012+
// it: one batched probe, read by both, never a second one.
10101013
const recordVerdict = useRecordCrudVerdicts({
10111014
objectName,
10121015
recordIds: pageRecordIds,
1013-
update: canEdit,
1016+
update: canEdit || inlineEditable,
10141017
delete: canDelete,
10151018
});
10161019

@@ -2546,6 +2549,15 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
25462549
reorderableColumns: schema.reorderableColumns ?? false,
25472550
// [#5143] The authored key ∧ this principal's write verdict on the object.
25482551
editable: inlineEditable,
2552+
// objectstack#27365 — …∧ the server's record-grained `update` verdict for
2553+
// THIS row, read through the very accessor the row kebab reads
2554+
// (`resolveRowRecordCrudAffordance` over `recordVerdict`), so the cells and
2555+
// the kebab give one answer for one row. Unknown (no answer yet, a failed
2556+
// probe, a row with no id) keeps the object-level answer: fail open, never
2557+
// over-hide. `editable` above is still the gate, so this only narrows.
2558+
isRowEditable: inlineEditable
2559+
? (row: any) => resolveRowRecordCrudAffordance(inlineEditable, recordVerdict(rowRecordId(row), 'update'))
2560+
: undefined,
25492561
// In-place cell editor: render the dedicated @object-ui/fields widget for
25502562
// the field's type — the SAME control the form uses (select→dropdown,
25512563
// boolean→checkbox, date→date picker, multi-select, …). Returning null lets

‎ui/packages/plugin-grid/src/__tests__/ObjectGrid.keyboardNavigation-11068.test.tsx‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* Each "off" reading is paired with an "on" reading on the same probe.
2828
*/
2929
import React from 'react';
30-
import { describe, it, expect, vi, afterEach } from 'vitest';
30+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
3131
import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
3232
import '@testing-library/jest-dom';
3333

@@ -54,6 +54,20 @@ vi.mock('@object-ui/permissions', async (importOriginal) => {
5454
import { ActionProvider, SchemaRenderer } from '@object-ui/react';
5555
// Registers `object-grid`, the block under test.
5656
import { ObjectGridRenderer } from '../index';
57+
import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts';
58+
import { installExplainDouble } from './explainDouble';
59+
60+
// objectstack#27365 — an editable grid asks the record-grained `update`
61+
// verdict for the rows on screen even with no kebab Edit wired. Answer it from
62+
// the double (every row permitted), never the network.
63+
beforeEach(() => {
64+
__clearRecordCrudVerdictCache();
65+
installExplainDouble();
66+
});
67+
afterEach(() => {
68+
cleanup();
69+
vi.unstubAllGlobals();
70+
});
5771

5872
afterEach(() => {
5973
cleanup();

‎ui/packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,28 @@
5555
* reference and the SAME records in ONE render, differing only in the declared
5656
* key — the shape objectui#6875 established and objectui#7154 reused.
5757
*/
58-
import { describe, it, expect, vi, beforeAll } from 'vitest';
59-
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
58+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
59+
import { render, screen, waitFor, fireEvent, within, cleanup } from '@testing-library/react';
6060
import '@testing-library/jest-dom';
6161
import React from 'react';
6262

6363
import { ObjectGrid } from '../ObjectGrid';
6464
import { registerAllFields } from '@object-ui/fields';
6565
import { ActionProvider, SchemaRendererProvider } from '@object-ui/react';
66+
import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts';
67+
import { installExplainDouble } from './explainDouble';
68+
69+
// objectstack#27365 — an editable grid asks the record-grained `update`
70+
// verdict for the rows on screen even with no kebab Edit wired. Answer it from
71+
// the double (every row permitted), never the network.
72+
beforeEach(() => {
73+
__clearRecordCrudVerdictCache();
74+
installExplainDouble();
75+
});
76+
afterEach(() => {
77+
cleanup();
78+
vi.unstubAllGlobals();
79+
});
6680

6781
registerAllFields();
6882

‎ui/packages/plugin-grid/src/__tests__/inlineEditDatePicker-12664.test.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,29 @@
2222
* The text cell at the bottom is the CONTROL: a widget that stages on every
2323
* keystroke commits exactly as before, with no focus and so no blur at all.
2424
*/
25-
import { describe, it, expect, vi, beforeAll } from 'vitest';
26-
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
25+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
26+
import { render, screen, waitFor, fireEvent, within, cleanup } from '@testing-library/react';
2727
import '@testing-library/jest-dom';
2828
import React from 'react';
2929

3030
import { ObjectGrid } from '../ObjectGrid';
3131
import { registerAllFields } from '@object-ui/fields';
3232
import { ActionProvider, SchemaRendererProvider } from '@object-ui/react';
3333
import { I18nProvider } from '@object-ui/i18n';
34+
import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts';
35+
import { installExplainDouble } from './explainDouble';
36+
37+
// objectstack#27365 — an editable grid asks the record-grained `update`
38+
// verdict for the rows on screen even with no kebab Edit wired. Answer it from
39+
// the double (every row permitted), never the network.
40+
beforeEach(() => {
41+
__clearRecordCrudVerdictCache();
42+
installExplainDouble();
43+
});
44+
afterEach(() => {
45+
cleanup();
46+
vi.unstubAllGlobals();
47+
});
3448

3549
registerAllFields();
3650

‎ui/packages/plugin-grid/src/__tests__/inlineEditExitMode.test.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,28 @@
1111
* fake server and assert the editor actually exits (its <input> / trigger is
1212
* gone) while the staged value is preserved (or reverted, for Escape).
1313
*/
14-
import { describe, it, expect, vi, beforeAll } from 'vitest';
15-
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
14+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
15+
import { render, screen, waitFor, fireEvent, within, cleanup } from '@testing-library/react';
1616
import '@testing-library/jest-dom';
1717
import React from 'react';
1818

1919
import { ObjectGrid } from '../ObjectGrid';
2020
import { registerAllFields } from '@object-ui/fields';
2121
import { ActionProvider, SchemaRendererProvider } from '@object-ui/react';
22+
import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts';
23+
import { installExplainDouble } from './explainDouble';
24+
25+
// objectstack#27365 — an editable grid asks the record-grained `update`
26+
// verdict for the rows on screen even with no kebab Edit wired. Answer it from
27+
// the double (every row permitted), never the network.
28+
beforeEach(() => {
29+
__clearRecordCrudVerdictCache();
30+
installExplainDouble();
31+
});
32+
afterEach(() => {
33+
cleanup();
34+
vi.unstubAllGlobals();
35+
});
2236

2337
registerAllFields();
2438

0 commit comments

Comments
 (0)