import type { ModelSchema } from './types'; import { type ResolvedOption } from '../use-options-resolver'; import type { VisibleWhen } from '../types'; import { type ItemField } from '../collection-cell'; import { type GetImageUrl } from '../image-url-context'; export type { GetImageUrl }; export interface FieldOption { value: string; label: string; /** * Pro option metadata the backend serves for enum/option fields (e.g. * `product_type`) so the view renders a colored/iconed badge instead of the * raw value ("storable" → "Almacenable"). All optional and driven entirely * by the served metadata — plain options stay plain. */ color?: string; icon?: string; image?: string; } export interface FieldDef { key: string; label: string; type: 'text' | 'textarea' | 'select' | 'search' | 'number' | 'date' | 'email' | 'url' | 'boolean' | 'image' | string; required?: boolean; options?: FieldOption[]; defaultValue?: any; placeholder?: string; readonly?: boolean; hidden?: boolean; searchEndpoint?: string; filterBy?: string; /** * FK target model the kernel auto-derives for a belongs_to column (>= * v0.46.x serves it on modal fields, not just action fields). When present * the native form renders an async searchable picker (`DynamicSelectField`) * against `/api/options/?field=id` — with option thumbnails when the * remote rows carry an `image` — instead of a raw FK text input. View mode * shows the resolved thumbnail + label. Tolerates the snake_case * `source`/`relation` aliases the manifest may serve. */ ref?: string; source?: string; relation?: string; /** * Explicit renderer hint. Wins over the `type` switch: `dynamic_select` * forces the searchable picker, `upload` forces the file dropzone. Lets the * kernel opt a plain text/uuid column into a rich widget without changing * its SQL type. Unknown values fall through to the `type`-based default. */ widget?: string; /** * Declarative display hint the backend stamps on the column/modal field * (mirrors the table column's `cellStyle`). `'currency'` makes the view * renderer format the numeric value in the org currency. Optional — * absent, a money-key heuristic still detects obvious money fields. */ cellStyle?: string; /** * Per-field style overrides served alongside `cellStyle` (e.g. * `{ currency: 'MXN' }`). When it carries an explicit `currency` it wins * over the org fallback. */ styleConfig?: Record; /** * Declared schema for a jsonb line-items field (kernel v3 `item_fields`). * The backend serves this on modal/detail fields the same way it does on * table columns. When present the read-only detail view renders the * `CollectionCell` mini-table with these (already-localized) headers in * order and resolves `ref` columns to the backend-injected sibling label. * Tolerates the snake_case `item_fields` the kernel serves. */ itemFields?: ItemField[]; /** snake_case alias served by the kernel for `itemFields`. */ item_fields?: ItemField[]; /** * Conditional visibility: render — and required-check — this field only * while a sibling field's current value matches the predicate. Mirrors the * kernel v3 `visible_when` (projected onto the served modal FieldDef). * Tolerates the camelCase alias. Absent = always visible; a hidden field is * dropped from the required-gate so it never blocks submit. Evaluated by * `evaluateVisibleWhen` against the live form values. */ visible_when?: VisibleWhen; /** camelCase alias for `visible_when`. */ visibleWhen?: VisibleWhen; /** * Form-layout membership: the key of the `form_layout` section this field * belongs to (kernel PR #230). Absent → the default group. See * `groupFieldsBySection`. */ section?: string; } export interface DynamicRecordDialogProps { open: boolean; onOpenChange: (open: boolean) => void; /** * Set by the host's inline-create bridge on the sibling "Crear" dialog: * marks this dialog as the nested-create SELF so the ui Dialog stamps * data-nested-inline-create (surgical focus release) and the body guard * lets it close while the depth lock is held. */ nestedInlineCreateSelf?: boolean; /** * Fields merged into the modal schema after load (by key). Existing keys are * shallow-merged; missing keys are prepended. Hosts use this to inject * required scope fields (e.g. branch_id) omitted from compiled DefineModal. */ ensureFields?: FieldDef[]; mode: 'view' | 'edit' | 'create'; model: string; recordId?: string | null; endpoint?: string; /** Fired after a successful save; receives the persisted record (when the * backend returns it) so callers — e.g. the inline-create bridge behind a * dynamic_select "+" — can auto-select the new row. */ onSaved?: (record?: any) => void; /** * Optional override invoked instead of the default `POST` when the dialog * is in `create` mode. Hosts may use this to route writes through custom * mutations (optimistic updates, audit hooks, etc.). The dialog still * closes and fires `onSaved` on success. */ onCreate?: (data: Record) => Promise<{ id?: string | number; } | void>; /** * Optional override invoked instead of the default `PUT` when the dialog * is in `edit` mode. Receives the record id and the form payload. */ onUpdate?: (recordId: string, data: Record) => Promise<{ id?: string | number; } | void>; /** * Optional default values seeded into the form on `create`. Ignored when * `mode` is `'edit'` or `'view'` (those fetch from the record endpoint). */ defaults?: Record; /** * Field keys that render locked (visible, disabled, seeded from * `defaults`) on create instead of editable. Ignored outside create mode. * See `CreateRecordDialogProps.lockedFields` for the rationale. */ lockedFields?: string[]; /** * Optional pre-fetched metadata. When provided the dialog skips the * `/metadata/modal/:model` request and uses this shape directly. */ schema?: ModelSchema; /** * Optional handler shown as a "Delete" action in `view` mode. The dialog * awaits the promise and closes on success. Omit to hide the action. */ onDelete?: () => Promise; /** * Optional handler shown as an "Edit" action in `view` mode. Omit to hide * the action. */ onEdit?: () => void; /** * Deliberate escape hatch: open the full `/m/:model/:id` detail page (with * cross-module related records) for records too heavy for the modal. * Rendered as a footer link in view mode when provided. */ onOpenFullPage?: () => void; /** * The row object the table already loaded. When provided, the dialog renders * instantly from it (no spinner) and reuses the table's pro siblings — the * resolved relation (`row.category = {value,label}`), served option lists and * image urls. A background fetch only fills in fields the list row omitted. */ initialRecord?: Record | null; /** * Host resolver turning a (possibly relative) storage path into a fetchable * URL for images/avatars/thumbnails. Defaults to identity. Pass the host's * `getImageUrl` so addon-served relative paths render. */ getImageUrl?: GetImageUrl; /** * Org IANA timezone (e.g. `America/Mexico_City`). Threaded into the tz-aware * `formatDateCell` so datetime/timestamp instants render in the org zone * regardless of the viewer's browser timezone. Pure `date` values pin to UTC. */ timeZone?: string; /** * Org ISO-4217 currency code (e.g. `MXN`) used as the fallback for money * fields (`cellStyle:'currency'` or the money-key heuristic) that lack an * explicit per-field currency. Optional — defaults to 'USD'. */ currency?: string; /** * Fired after a child relation row (line item, etc.) is created/updated/ * deleted from within the dialog. The dialog ALREADY refetches its own * parent record so server-recomputed rollups (sub_total, tax_amount, total) * appear in place — this callback additionally lets the host invalidate its * own list/detail query so the parent row's totals refresh underneath. */ onChange?: () => void; } export declare function objectLabel(value: any): string | undefined; export declare function relationSiblingValue(field: FieldDef, record: any): any; export declare function fieldItemFields(field: FieldDef): ItemField[] | undefined; export declare function isLineItemsField(field: FieldDef, value: any): boolean; export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null; export declare function isMoneyField(field: FieldDef, value: any): boolean; export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create', formValues?: Record): FieldDef[]; export declare function stripHiddenFieldValues(values: Record, fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): Record; export declare function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, ensureFields, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element; export declare function ReadonlyEditField({ field, value }: { field: FieldDef; value: any; }): import("react").JSX.Element; export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: { field: FieldDef; value: any; record: any; /** Optional override; when omitted falls back to the nearest provider/identity. */ getImageUrl?: GetImageUrl; /** Optional override; when omitted falls back to the nearest provider. */ timeZone?: string; /** Optional override; when omitted falls back to the nearest provider. */ currency?: string; }): import("react").JSX.Element; export declare function EditField({ field, value, onChange, record, invalid }: { field: FieldDef; value: any; onChange: (val: any) => void; /** The full record being edited — supplies FK relation siblings + line-items. */ record?: any; /** When true, paint the control with a destructive border (Laravel-style). */ invalid?: boolean; }): import("react").JSX.Element; //# sourceMappingURL=dynamic-record.d.ts.map