import { default as React } from 'react'; import { FieldWidgetComponentProps } from './types.js'; /** * GridField / LineItemsField — editable child-grid ("line items") widget. * * ## Where the host's label and help text land, and why (objectui#4857) * * This widget forwarded nothing a host handed it, so the field's visible label * pointed `for` at an id no element carried and the help text had zero * consumers. Measured on `origin/main` at `e71c854ce`, a real form, editable * state, `description` set: * * ``` * bare (no columns) for=DANGLING hostIdEl=NONE consumers=0 focusables=[button] * columns + one row for=DANGLING hostIdEl=NONE consumers=0 focusables=[drag, * input(Item), input(Qty), button(Duplicate row), * button(Remove row), input(Item), input(Qty), button(Add)] * ``` * * The bare row's single focusable is the auxiliary "Add line" BUTTON — labelable, * but routing the host id (and so the label's `for`) onto it would make the * field's label NAME the add-row action and make a click on the label insert a * row. Every realistic config is a composite: many cell inputs, each with its * own `aria-label`, under one container. So this widget declares * `labelling: 'group'` (see `FIELD_WIDGET_LABELLING` in `../index`) — the * objectui#3961 composite shape, like `address` — and the CONTAINER consumes the * host's keys: * * - editable / list mode: the root div takes the DOM pass-through minus `name` * (DOM-legal on form controls only — the objectui#3291 leak) and minus * `aria-invalid` (control-channel state; this grid reports validity per CELL, * with its own inline marks), answering `role="group"` only when a host * actually named it — `CheckboxesField`'s split, key for key. * * That strip STANDS (objectui#3318 upheld it rather than overturning it): * the container is not a control, so the host's state is not re-routed onto * it. What #3318 added is the other half the strip implied but nobody had * built — the host's failure now DRIVES the per-cell channel this comment * already claimed as the reporting path. See `hostFailedEmpty`. * - readonly: the table replaces the inputs entirely, so that surface takes the * name AND the description via `toHostGroupProps` — `'instead-of-the-inputs'`. * * A controlled component: `value` is an array of row objects, `onChange` * receives the next array. It renders one editable cell per configured * column, supports add / delete row, and shows a running total of a numeric * column. This is the renderer for the `field:grid` widget and the cell * engine behind the master-detail subform (see ADR-0001). * * Column config (a subset of `GridColumnDefinition`): * { name, label?, type?, options?, width?, required?, prefix?, step? } * type ∈ 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' * | 'select' | 'lookup' | 'file' * * Field-level config (from `GridFieldMetadata`): * columns, min_rows, max_rows, allow_add, allow_delete, total_field */ export interface GridColumn { /** * The column's field name — the key it reads and writes on each row object. * * Spelled `name`, exactly as the declared `GridColumnDefinition` * (`@object-ui/types`) and the grid docs page say (objectui#3951). This * widget used to read a divergent `field` key, so metadata authored against * the published type rendered every cell empty plus a React key warning. * There is deliberately no tolerant alias bridging the retired spelling to * this one: a single spelling, enforced at the producer — AGENTS.md #0.1. * * (Wording note: do not restate that rule as an alternation expression over * the two key names. `column-identity.ratchet.test.ts` (objectui#3104) scans * these files line by line and cannot tell prose from code, so spelling the * shape out here registers as a new dual read and fails the gate.) */ name: string; label?: string; /** * Cell control + read/write adapter for the column. * * `date` / `datetime` / `time` are three DISTINCT controls, not one * (objectui#3569). Collapsing `datetime` onto the `date` control did not * merely under-render it — `` hands back a bare * `YYYY-MM-DD` on change, so touching the day of a `datetime` cell silently * DELETED its time component from the record. */ type?: 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'; options?: Array<{ label: string; value: string; }>; width?: number; required?: boolean; prefix?: string; step?: number; /** For `type: 'lookup'` — the referenced object and label/id fields. */ reference?: string; displayField?: string; idField?: string; /** Multi-value column: multi-record lookup, or multi-file upload cell. */ multiple?: boolean; /** For `type: 'file'` — accepted MIME types / extensions for the picker * (e.g. `['image/*', '.pdf']`). Omit to accept anything. */ accept?: string[]; /** * Hidden from the grid by default but revealable via the column chooser. * Set by `deriveColumns` for fields beyond the default-visible budget — the * data is NOT dropped (it's just collapsed, like Odoo's `optional` columns / * Salesforce column personalization), so business-critical fields stay * reachable. Required columns are never default-hidden. */ defaultHidden?: boolean; /** * A computed (read-only) column whose value is derived live from sibling * cells via {@link expr} — e.g. an invoice line's `amount = quantity * * unit_price`. The grid renders it read-only, recomputes it as the row's * inputs change, and writes the result back into the row so it persists * (and any running total reflects it). The classic spreadsheet pattern used * by QuickBooks / Stripe / NetSuite line grids — nobody types the amount. */ computed?: boolean; /** Arithmetic expression for a {@link computed} column. Supports `+ - * / %`, * parentheses, numeric literals and field refs (`record.qty` or bare `qty`). */ expr?: string; /** Decimal places to round a computed numeric/currency result to. */ scale?: number; /** For `type: 'lookup'` — when a record is picked, copy its fields into any * sibling columns of the same name (e.g. a product's unit_price/description). * On by default for lookup columns; set `false` to disable the auto-fill. */ autofill?: boolean; /** * CEL predicate: when TRUE for this row, the cell is **read-only** (B2 field * rules, generalized to grid cells). Evaluated per row against the row as * `record` plus the header as `parent` (so a line locks when * `parent.status == 'paid'` *or* on an intra-row condition like * `record.kind == 'auto'`). Client-side UX; fails open (stays editable). */ readonlyWhen?: string | { dialect?: string; source: string; }; /** * CEL predicate: when TRUE for this row, the cell is **required** (flagged * inline-invalid while empty). Same `record` + `parent` scope as * {@link readonlyWhen}. */ requiredWhen?: string | { dialect?: string; source: string; }; } type Row = Record; /** Evaluate an arithmetic `expr` against `row`. null when blank/unparseable. */ export declare function evalArith(expr: string, row: Row): number | null; /** * Recompute every {@link GridColumn.computed} cell in `row` from its sibling * inputs, returning a new row. Called after each edit so computed columns and * the running total stay live, and so the computed value persists in the batch. */ /** * Build the row patch for a lookup-cell selection: set the FK column to the * chosen record's id, and (unless the column opts out with `autofill: false`) * copy any of the record's fields whose names match a sibling column — the * catalog-typeahead behaviour (pick a product → its unit_price/description fill * in). Skips the lookup column itself and computed columns. Pure + exported so * it is unit-testable independent of the picker UI. */ export declare function lookupAutofillPatch(columns: GridColumn[], col: GridColumn, record: any): Row; export declare function computeRow(columns: GridColumn[], row: Row): Row; /** Sum a numeric column across rows (ignoring blanks/NaN). */ export declare function sumColumn(rows: Row[], field: string): number; export declare function GridField({ value, onChange, field, readonly, disabled, className, error, onRowExpand, displayMode, onAdd, ...props }: FieldWidgetComponentProps & { /** When provided, each row shows an "expand" button that opens the row in a * full form (the host — e.g. MasterDetailForm — renders the drawer/modal and * writes the edited values back). Lets a "fat" child be edited in a real form * while the grid stays a quick at-a-glance editor. */ onRowExpand?: (rowIndex: number) => void; /** 'grid' (default) = editable cells; 'list' = read-only rows whose primary * action is per-row edit (via `onRowExpand`) and whose Add opens a new row * in the full form (via `onAdd`). The form-factor for "fat" children. */ displayMode?: 'grid' | 'list'; /** In 'list' mode, "Add" calls this (host opens the full form for a new row) * instead of inserting a blank inline row. */ onAdd?: () => void; /** The header/parent record, bound as `parent` when evaluating a column's * `readonlyWhen` / `requiredWhen` CEL predicate — so a line cell can react to * the header (`parent.status == 'paid'`). Supplied by MasterDetailForm. */ contextRecord?: Record; }): React.JSX.Element; /** Semantic alias — the master-detail subform's child grid. */ export declare const LineItemsField: typeof GridField; export {};