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 {};