diff --git a/.changeset/11070-grid-field-keys-round8.md b/.changeset/11070-grid-field-keys-round8.md new file mode 100644 index 0000000000..6c0634624e --- /dev/null +++ b/.changeset/11070-grid-field-keys-round8.md @@ -0,0 +1,26 @@ +--- +'@object-ui/types': minor +'@object-ui/fields': minor +'@object-ui/plugin-form': patch +--- + +The grid field reads each field-level key under the one spelling `GridFieldMetadata` declares (objectui#11070, round 8). + +`GridFieldMetadata` declared `allow_reorder` and the docs taught it, while `GridField` read `reorderable`, so `allow_reorder: false` still drew a drag handle on every row. The widget also read its footer total under three spellings, and read four keys that no face declared. Each key now has one spelling, and that spelling is declared and read: + +- **Reorder (`@object-ui/fields`).** `GridField` reads `allow_reorder`. `allow_reorder: false` removes the drag handles. The undeclared `reorderable` is no longer read; this round's census found no writer of it in either repository. +- **Total (`@object-ui/types`, `@object-ui/fields`).** `GridFieldMetadata` declares `total_field`, the one spelling the grid reads. It names the CHILD column summed into the footer, which is the spec's `amountField` (`inlineAmountField` on a `master_detail` field, `subforms[].amountField` on a form view). It is not the spec's `totalField`, the parent field a master-detail save writes the sum to. The `amount_field` and `amountField` reads beside it are retired: the same census found nothing writing either into the grid's config. +- **`add_label` (`@object-ui/types`).** Declared. `MasterDetailForm` writes it from a detail's `addLabel`, and it labels the grid's Add button. +- **`allow_duplicate` and `show_line_numbers` (`@object-ui/fields`)** are retired under ADR-0049. No face declared either, and the census found no producer of either. The behaviour their defaults gave stays: each row offers a duplicate action whenever rows can be added, and the line-number column always shows. +- **`sort_field` is unchanged.** `MasterDetailForm` still writes it from a detail's `sortField`, which is derived from the child object when not authored. The spec declares no inline sort-field key, so it stays read and undeclared, named in one place in `GridField`. +- **`record:line_items` total (`@object-ui/plugin-form`).** The panel shows its grid's footer total whenever `amountField` names the column to sum, the way `MasterDetailForm` already did. It used to show it only when `totalField` was also set. + +`GridField` now types its config reads as `GridFieldMetadata`, so a read of a key the type does not declare fails to compile. + +**Clause-②: yes (narrowing).** The published `GridFieldMetadata` face widens by two optional members, `total_field` and `add_label`. What the grid honours narrows: five keys it used to read are no longer read. + +## ⚠️ BREAKING, priced as minor under the fixed group's version policy + +- **Rendering.** A grid field written with `reorderable`, `amount_field`, `amountField`, `allow_duplicate` or `show_line_numbers` renders as if that key were absent. Fix: write `allow_reorder: false` to turn off drag reordering, and `total_field` to name the summed column. To turn off the duplicate action, turn off adding with `allow_add: false`; there is no switch for the line-number column. +- **Behaviour.** `allow_reorder: false` now removes the drag handles; before, it was ignored. A `record:line_items` panel with `amountField` and no `totalField` now shows the footer total of that column. +- **TypeScript.** A `GridFieldMetadata` literal carrying any of the five retired keys was already a compile error, and still is. diff --git a/content/docs/fields/grid.mdx b/content/docs/fields/grid.mdx index 6f8f3f75f5..f382d59349 100644 --- a/content/docs/fields/grid.mdx +++ b/content/docs/fields/grid.mdx @@ -20,12 +20,12 @@ The Grid Field component provides an inline table for managing related records o ## Field Schema A grid field is authored as `GridFieldMetadata` (`@object-ui/types`), which is the -source of truth for the key set: it extends `BaseFieldMetadata` with the column list -and the row-count and row-action limits. Each column is `@objectstack/spec`'s inline -grid column (`InlineGridColumn`, the element of the spec's `inlineColumns` list on a -`master_detail` field), typed by reference, so the columns are checked by the same -compiler that checks the field, and `objectui validate` refuses a column key the spec -does not declare. +source of truth for the key set: it extends `BaseFieldMetadata` with the column list, +the row-count and row-action limits, the footer total and the Add button's label. +Each column is `@objectstack/spec`'s inline grid column (`InlineGridColumn`, the +element of the spec's `inlineColumns` list on a `master_detail` field), typed by +reference, so the columns are checked by the same compiler that checks the field, +and `objectui validate` refuses a column key the spec does not declare. ```ts import type { GridFieldMetadata } from '@object-ui/types'; @@ -38,15 +38,35 @@ const lineItems: GridFieldMetadata = { { name: 'product', label: 'Product', type: 'lookup', required: true, width: 240 }, { name: 'quantity', label: 'Qty', type: 'number', width: 80 }, { name: 'unit_price', label: 'Unit Price', type: 'currency', width: 120 }, + { name: 'amount', label: 'Amount', type: 'currency', computed: true, expr: 'quantity * unit_price' }, ], min_rows: 1, max_rows: 50, allow_add: true, allow_delete: true, allow_reorder: false, + total_field: 'amount', + add_label: 'Add line', }; ``` +The field-level keys, each read under exactly this one spelling: + +- `min_rows` / `max_rows` — the row-count limits. +- `allow_add` / `allow_delete` — whether rows can be added or removed. Each row's + duplicate action follows `allow_add`, since a duplicate is an add. +- `allow_reorder` — set `false` to remove the drag handles; rows can be reordered + by dragging otherwise. +- `total_field` — the `name` of the **child** column summed into the footer total. + It is the spec's `amountField`, not its `totalField` (the parent field a + master-detail save writes the sum to). No total shows when it is unset. +- `add_label` — the Add button's label. + +A read-only or disabled grid offers no add, delete, duplicate or reorder, whatever +these keys say. The line-number column always shows. There is no `reorderable`, +`amount_field`, `amountField`, `allow_duplicate` or `show_line_numbers` key: none +is read, and `GridFieldMetadata` refuses each. + A column's `width` is a **number** of pixels. There is no per-column `editable` key (whether cells can be edited follows the field's own read-only state) and no per-column `defaultValue`: a new row starts with every cell empty. diff --git a/packages/fields/src/widgets/GridField.fieldKeys-11070.test.tsx b/packages/fields/src/widgets/GridField.fieldKeys-11070.test.tsx new file mode 100644 index 0000000000..05552e1de8 --- /dev/null +++ b/packages/fields/src/widgets/GridField.fieldKeys-11070.test.tsx @@ -0,0 +1,165 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The grid widget's FIELD-level keys: one spelling each, and that spelling is + * the one `GridFieldMetadata` (`@object-ui/types`) declares (objectui#11070, + * round 8). + * + * Before this round the published type and the widget disagreed in both + * directions: `GridFieldMetadata.allow_reorder` was declared and taught by the + * docs while the widget read `reorderable`, so `allow_reorder: false` still + * drew a drag handle on every row; the total was read under three spellings + * (`total_field`, `amount_field`, `amountField`); and four keys were read that + * no face declared (`allow_duplicate`, `show_line_numbers`, `add_label`, and + * `sort_field`, which this file does not cover — see the changeset). + * + * What each block pins: + * + * - reorder — `allow_reorder: false` removes the drag handle from every row, + * against a no-key control that draws one per row; the retired + * `reorderable` changes nothing; + * - total — `total_field` names the CHILD column summed into the footer + * (the spec's `amountField`, never its `totalField`); `amount_field` and + * `amountField` change nothing; + * - `add_label` — declared, and it labels the Add button; + * - `allow_duplicate` and `show_line_numbers` — retired under ADR-0049 (no + * producer in either repository wrote them): the widget keeps the + * behaviour their defaults gave, a duplicate action whenever rows can be + * added and a line-number column always. + * + * Every fixture is typed `GridFieldMetadata`, so `tsc -p tsconfig.test.json` + * (this package's `type-check`) also holds the declared key set: a declared + * key that stopped being a member reddens the fixture that writes it, and a + * retired key that came back as a member reddens its `@ts-expect-error`. + * A retired key reaches the widget only through a cast, which is how a + * document the compiler never saw would reach it. + */ + +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import type { GridFieldMetadata } from '@object-ui/types'; +import { GridField } from './GridField'; + +const columns: NonNullable = [ + { name: 'description', label: 'Description', type: 'text' }, + { name: 'amount', label: 'Amount', type: 'currency' }, +]; + +const rows = [ + { description: 'A', amount: 10 }, + { description: 'B', amount: 20 }, +]; + +/** A grid field as `GridFieldMetadata` declares it, plus the keys under test. */ +function grid(keys: Partial> = {}): GridFieldMetadata { + return { type: 'grid', name: 'lines', columns, ...keys }; +} + +/** A key the type does not declare, reaching the widget the way an unchecked document would. */ +function withUndeclared(extra: Record): GridFieldMetadata { + return { ...grid(), ...extra } as GridFieldMetadata; +} + +function show(field: GridFieldMetadata) { + return render( {}} field={field} />); +} + +const dragHandles = () => screen.queryAllByTestId(/^line-items-drag-/).length; + +describe('GridField field-level keys: the declared spelling is the read spelling (objectui#11070 round 8)', () => { + describe('reorder: `allow_reorder`', () => { + it('CONTROL: with no key, every row draws a drag handle', () => { + show(grid()); + expect(dragHandles()).toBe(rows.length); + }); + + it('`allow_reorder: false` draws no drag handle on any row', () => { + show(grid({ allow_reorder: false })); + expect(dragHandles()).toBe(0); + }); + + it('the retired `reorderable` is not read: `reorderable: false` still draws every handle', () => { + show(withUndeclared({ reorderable: false })); + expect(dragHandles()).toBe(rows.length); + }); + }); + + describe('total: `total_field` names the child column summed', () => { + it('`total_field` shows the footer total of that child column', () => { + show(grid({ total_field: 'amount' })); + expect(screen.getByTestId('line-items-total').textContent).toContain('30'); + }); + + it('CONTROL: with no key there is no footer total', () => { + show(grid()); + expect(screen.queryByTestId('line-items-total')).toBeNull(); + }); + + it.each(['amount_field', 'amountField'])('the retired `%s` is not read: no footer total', (key) => { + show(withUndeclared({ [key]: 'amount' })); + expect(screen.queryByTestId('line-items-total')).toBeNull(); + }); + }); + + describe('`add_label`', () => { + it('labels the Add button', () => { + show(grid({ add_label: 'Add invoice line' })); + expect(screen.getByTestId('line-items-add').textContent).toContain('Add invoice line'); + }); + }); + + describe('`allow_duplicate` is retired: duplicate follows whether rows can be added', () => { + it('`allow_duplicate: false` is not read: each row keeps its duplicate action', () => { + show(withUndeclared({ allow_duplicate: false })); + expect(screen.queryAllByTestId(/^line-items-duplicate-/)).toHaveLength(rows.length); + }); + + it('CONTROL: `allow_add: false` removes the duplicate action with the Add action', () => { + show(grid({ allow_add: false })); + expect(screen.queryAllByTestId(/^line-items-duplicate-/)).toHaveLength(0); + }); + }); + + describe('`show_line_numbers` is retired: the line-number column always shows', () => { + it('`show_line_numbers: false` is not read: the `#` column stays', () => { + show(withUndeclared({ show_line_numbers: false })); + expect(screen.getByRole('columnheader', { name: '#' })).toBeTruthy(); + }); + }); +}); + +// ── The declared key set, held by the compiler (`tsc -p tsconfig.test.json`) ── + +/** Every field-level key the widget reads, each typed as `GridFieldMetadata` declares it. */ +const declared: GridFieldMetadata = { + type: 'grid', + name: 'lines', + columns, + min_rows: 1, + max_rows: 50, + allow_add: true, + allow_delete: true, + allow_reorder: false, + total_field: 'amount', + add_label: 'Add line', +}; +void declared; + +// @ts-expect-error objectui#11070 round 8: `reorderable` is retired; the reorder key is `allow_reorder`. +const retiredReorderable: GridFieldMetadata = { type: 'grid', name: 'lines', reorderable: false }; +// @ts-expect-error objectui#11070 round 8: `amount_field` is retired; the summed child column is `total_field`. +const retiredAmountSnake: GridFieldMetadata = { type: 'grid', name: 'lines', amount_field: 'amount' }; +// @ts-expect-error objectui#11070 round 8: `amountField` is retired on the widget; the summed child column is `total_field`. +const retiredAmountCamel: GridFieldMetadata = { type: 'grid', name: 'lines', amountField: 'amount' }; +// @ts-expect-error objectui#11070 round 8: `allow_duplicate` is retired (ADR-0049); duplicate follows `allow_add`. +const retiredDuplicate: GridFieldMetadata = { type: 'grid', name: 'lines', allow_duplicate: false }; +// @ts-expect-error objectui#11070 round 8: `show_line_numbers` is retired (ADR-0049); the line-number column always shows. +const retiredLineNumbers: GridFieldMetadata = { type: 'grid', name: 'lines', show_line_numbers: false }; +void [retiredReorderable, retiredAmountSnake, retiredAmountCamel, retiredDuplicate, retiredLineNumbers]; diff --git a/packages/fields/src/widgets/GridField.tsx b/packages/fields/src/widgets/GridField.tsx index dc5b9cc15b..251b2cbe75 100644 --- a/packages/fields/src/widgets/GridField.tsx +++ b/packages/fields/src/widgets/GridField.tsx @@ -26,6 +26,7 @@ import { useFieldTranslation } from './useFieldTranslation.js'; import { toDomProps } from './toDomProps.js'; import { toHostGroupProps } from './toHostGroupProps.js'; import type { InlineGridColumn } from '@objectstack/spec/data'; +import type { GridFieldMetadata } from '@object-ui/types'; /** * GridField / LineItemsField — editable child-grid ("line items") widget. @@ -76,8 +77,9 @@ import type { InlineGridColumn } from '@objectstack/spec/data'; * Column config: `@objectstack/spec`'s inline grid column, by reference — * see {@link GridColumn}. * - * Field-level config (from `GridFieldMetadata`): - * columns, min_rows, max_rows, allow_add, allow_delete, total_field + * Field-level config: the keys `GridFieldMetadata` (`@object-ui/types`) + * declares, each read under that one spelling (objectui#11070 round 8), plus + * the one read key no face declares yet — see {@link UndeclaredGridKeys}. */ /** @@ -131,6 +133,22 @@ import type { InlineGridColumn } from '@objectstack/spec/data'; */ export type GridColumn = InlineGridColumn; +/** + * The field-level key this widget reads that no face declares. + * + * `sort_field` names the child field the grid stamps with each row's position + * on every change, so a drag-reorder survives a reload. It has a live producer: + * `MasterDetailForm` (`@object-ui/plugin-form`) writes it from its detail's + * `sortField`, which `deriveDetail` derives from the child object's fields (a + * `position` / `sort_order` / … field) when none is authored. The spec declares + * no inline sort-field key, so whether it becomes a declared key is a protocol + * question, held open on objectui#11070 (round 8) rather than decided here. + * Until then it is named in this one place, not read through an `any`. + */ +interface UndeclaredGridKeys { + sort_field?: string; +} + type Row = Record; const isNumeric = (t?: string) => t === 'number' || t === 'currency'; @@ -578,7 +596,10 @@ export function GridField({ * the header (`parent.status == 'paid'`). Supplied by MasterDetailForm. */ contextRecord?: Record; }) { - const cfg = (field || {}) as any; + // The field-level keys, read as `GridFieldMetadata` declares them, so a read + // of a key the type does not declare is a compile error here rather than a + // second, unpublished contract (objectui#11070 round 8). + const cfg = (field || {}) as Partial & UndeclaredGridKeys; const allColumns: GridColumn[] = cfg.columns || []; const rows: Row[] = Array.isArray(value) ? value : []; const contextRecord = props.contextRecord; @@ -660,21 +681,35 @@ export function GridField({ const allowAdd = cfg.allow_add !== false && !readonly && !disabled; const allowDelete = cfg.allow_delete !== false && !readonly && !disabled; - const allowDuplicate = cfg.allow_duplicate !== false && allowAdd; + // A duplicate IS an add, so it is offered exactly when adding is. There is + // no key of its own: `allow_duplicate` was read here while no face declared + // it and round 8's census of both repositories found no producer of it, so + // it was retired under ADR-0049 (objectui#11070 round 8), keeping the + // behaviour its default gave. + const allowDuplicate = allowAdd; // Per-row "expand to full form" (mainstream hybrid: quick grid + rich form). const showExpand = typeof onRowExpand === 'function' && !readonly; - // Enterprise line grids (NetSuite/SAP/Salesforce) show a line-number column. - const showLineNumbers = cfg.show_line_numbers !== false; + // Enterprise line grids (NetSuite/SAP/Salesforce) show a line-number column, + // always: the `show_line_numbers` switch was retired with `allow_duplicate`, + // for the same reason (objectui#11070 round 8). const minRows: number = cfg.min_rows ?? 0; const maxRows: number | undefined = cfg.max_rows; - const totalField: string | undefined = - cfg.total_field || cfg.amount_field || cfg.amountField; + // The CHILD column summed into the footer: the spec's `amountField` (an + // `inlineAmountField` / `subforms[].amountField`), which both adapters in + // `@object-ui/plugin-form` write here. ⛔ Not the spec's `totalField`, the + // PARENT field that receives the rollup on save. One spelling: the + // `amount_field` / `amountField` reads beside it are retired, round 8's + // census having found no producer of either (objectui#11070 round 8). + const totalField: string | undefined = cfg.total_field; // When set, the row's order is persisted by stamping `row[sortField] = index` // on every change — so drag-reorder survives a reload (the app adds a numeric // position field and lists sort by it). Without it, reorder is order-of-entry. const sortField: string | undefined = cfg.sort_field; - // Drag-to-reorder is on for editable grids (off in read-only / list mode). - const allowReorder = cfg.reorderable !== false && !readonly && !disabled; + // Drag-to-reorder is on for editable grids (off in read-only / list mode), + // and `allow_reorder: false` turns it off: the key `GridFieldMetadata` + // declares. The undeclared `reorderable` this used to read is retired + // (objectui#11070 round 8). + const allowReorder = cfg.allow_reorder !== false && !readonly && !disabled; const emit = useCallback( (next: Row[]) => { @@ -880,9 +915,7 @@ export function GridField({ - {showLineNumbers && ( - - )} + {columns.map((c) => ( - {showLineNumbers && ( - - )} + {columns.map((c) => (
## {t('fields.grid.noItems', { defaultValue: 'No items' })} @@ -910,9 +943,7 @@ export function GridField({ ) : ( rows.map((row, rowIdx) => (
{rowIdx + 1}{rowIdx + 1}
{totalLabel} @@ -1192,9 +1223,7 @@ export function GridField({ - {showLineNumbers && ( - - )} + {columns.map((c) => ( - )} + {columns.map((c, colIdx) => { // Inline validation: a required, non-computed cell that's // empty flags red in place. The "required" verdict honors @@ -1363,7 +1390,7 @@ export function GridField({
## {t('fields.grid.noItemsAddHint', { @@ -1242,26 +1271,24 @@ export function GridField({ } : {})} > - {showLineNumbers && ( - - - {reorderable && ( - { dragIndex.current = rowIdx; }} - onDragEnd={() => { dragIndex.current = null; }} - className="cursor-grab text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" - title={dragLabel} - aria-label={dragLabel} - data-testid={`line-items-drag-${rowIdx}`} - > - - - )} - {rowIdx + 1} - - + + {reorderable && ( + { dragIndex.current = rowIdx; }} + onDragEnd={() => { dragIndex.current = null; }} + className="cursor-grab text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100" + title={dragLabel} + aria-label={dragLabel} + data-testid={`line-items-drag-${rowIdx}`} + > + + + )} + {rowIdx + 1} + +
{totalLabel} diff --git a/packages/plugin-form/src/LineItemsPanel.gridTotal-11070.test.tsx b/packages/plugin-form/src/LineItemsPanel.gridTotal-11070.test.tsx new file mode 100644 index 0000000000..32d92790c1 --- /dev/null +++ b/packages/plugin-form/src/LineItemsPanel.gridTotal-11070.test.tsx @@ -0,0 +1,78 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `record:line_items` shows its grid's footer total whenever `amountField` + * names the child column to sum (objectui#11070 round 8). + * + * The grid's `total_field` is the CHILD column summed. `MasterDetailForm` maps + * it from the detail's `amountField`; this panel used to map it only when + * `totalField` (the PARENT field the sum is saved to) was set as well, so a + * panel that named only the column to sum (the objectstack showcase's + * project page authors exactly that) showed no total. Both adapters now map + * the same key the same way. + */ + +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { registerAllFields } from '@object-ui/fields'; +import { SchemaRendererProvider } from '@object-ui/react'; +import { LineItemsPanel, type LineItemsPanelSchema } from './LineItemsPanel'; + +registerAllFields(); + +function dataSource() { + return { + getObjectSchema: vi.fn().mockResolvedValue(null), + find: vi.fn().mockResolvedValue({ + data: [ + { id: 'l1', hours: 3 }, + { id: 'l2', hours: 4 }, + ], + }), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + } as any; +} + +const BASE: LineItemsPanelSchema = { + childObject: 'task', + relationshipField: 'project', + parentObject: 'project', + parentId: 'p1', + columns: [{ name: 'hours', label: 'Hours', type: 'number' }], +}; + +function show(schema: LineItemsPanelSchema) { + render( + + + , + ); +} + +/** Wait for the rows to load, so an absent total is not just an unfinished load. */ +async function loaded() { + await waitFor(() => expect(screen.getAllByLabelText('Hours').length).toBeGreaterThanOrEqual(2)); +} + +describe('record:line_items: the grid total follows `amountField` (objectui#11070 round 8)', () => { + it('`amountField` alone shows the footer total of that child column', async () => { + show({ ...BASE, amountField: 'hours' }); + await loaded(); + expect(screen.getByTestId('line-items-total').textContent).toBe('7'); + }); + + it('CONTROL: with neither key there is no footer total', async () => { + show(BASE); + await loaded(); + expect(screen.queryByTestId('line-items-total')).toBeNull(); + }); +}); diff --git a/packages/plugin-form/src/LineItemsPanel.tsx b/packages/plugin-form/src/LineItemsPanel.tsx index be1ba5cfa5..57b239f970 100644 --- a/packages/plugin-form/src/LineItemsPanel.tsx +++ b/packages/plugin-form/src/LineItemsPanel.tsx @@ -96,7 +96,9 @@ export interface LineItemsPanelSchema { parentObject?: string; parentId?: string; recordId?: string; + /** Numeric CHILD column summed into the grid's footer total, e.g. `'amount'`. */ amountField?: string; + /** PARENT field that receives that sum on save, e.g. `'total_amount'`. */ totalField?: string; title?: string; readonly?: boolean; @@ -700,7 +702,11 @@ export const LineItemsPanel: React.FC<{ schema: LineItemsPanelSchema }> = ({ sch // the surrounding form already disables that field (objectui#10163). // Adding and removing lines stay on `schema.readonly` below. columns: applyColumnPermissions(schema.columns, { perms, objectName: schema.childObject }), - total_field: schema.totalField ? schema.amountField || 'amount' : undefined, + // The grid's `total_field` is the CHILD column summed (this block's + // `amountField`), shown whenever one is named, exactly as + // `MasterDetailForm` maps it; `totalField` is only the PARENT field + // the sum is written to on save (objectui#11070 round 8). + total_field: schema.amountField || (schema.totalField ? 'amount' : undefined), min_rows: schema.minRows, max_rows: schema.maxRows, allow_add: !schema.readonly, diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 73f6bd0aef..35d8ea3977 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -1041,9 +1041,32 @@ export interface GridFieldMetadata extends BaseFieldMetadata { */ allow_delete?: boolean; /** - * Whether to allow reordering rows + * Whether rows can be reordered by dragging. On unless set `false`; a + * read-only or disabled grid never offers it. The one reorder key the grid + * reads (objectui#11070 round 8 retired the undeclared `reorderable` it used + * to read instead, which left this member taught and ignored). */ allow_reorder?: boolean; + /** + * The CHILD column whose values are summed into the grid's footer total — + * the `name` of one of {@link columns}. No total shows when it is unset. + * + * It is the spec's `amountField` (`FieldSchema.inlineAmountField` on a + * `master_detail` field, `subforms[].amountField` on a form view): the + * master-detail and line-items adapters in `@object-ui/plugin-form` write + * that key here. ⛔ It is NOT the spec's `totalField`, the PARENT field that + * receives the rolled-up sum on save; the grid never writes the parent. The + * one spelling the grid reads (objectui#11070 round 8 retired its + * `amount_field` / `amountField` reads, which nothing produced). + */ + total_field?: string; + /** + * Label of the grid's Add button, and the label its empty state names. The + * locale's own wording shows when it is unset. It is the spec's + * `subforms[].addLabel`, which the master-detail adapter in + * `@object-ui/plugin-form` writes here. + */ + add_label?: string; } export interface ColorFieldMetadata extends BaseFieldMetadata {