// Shared metadata shape consumed by every host. Some hosts add a `link` // action type with a `linkUrl` template — represented here as part of the // `type` union so the SDK can render it uniformly. /** One spreadsheet column of a model's import template. Mirrors the kernel's * `modelbase.ImportColumn`. */ export interface ImportColumnMeta { key: string header: string aliases?: string[] required?: boolean type?: string example?: string hint?: string generator?: string } /** A model's spreadsheet-import declaration. Mirrors `modelbase.ImportSpec`. */ export interface ImportSpecMeta { columns: ImportColumnMeta[] maxRows?: number sheetName?: string instructions?: string[] } export interface TableMetadata { title: string endpoint: string columns: ColumnDefinition[] actions: ActionDefinition[] filters?: FilterDefinition[] perPageOptions: number[] defaultPerPage: number searchPlaceholder: string enableCRUDActions: boolean hasActions: boolean canExport?: boolean canImport?: boolean canCreate?: boolean /** * The model's spreadsheet-import declaration, served by the kernel: the * columns of the generated template and the headers accepted when parsing * a filled file back in. The kernel derives it from the model's form * fields when the model declares nothing, and omits the field entirely * when the model has no importable column — so its presence is the signal * that importing is meaningful for this model. Purely additive; older * kernels omit it. */ import?: ImportSpecMeta /** * Child relations of this model, served by the kernel (>= v0.41.0). A * generic detail page renders one `DynamicRelation` panel per entry via * `` to surface, e.g., a Customer's vehicles, addresses * and attachments. Absent on hosts/older kernels — purely additive. */ relations?: RelationMeta[] /** * Which renderer the host should use for this view. `'table'` (default, or * absent) → `DynamicTable`; `'kanban'` → `DynamicKanban`. Served by the * kernel from the nav item's `view_type` (RFC §1.2). Purely additive — older * kernels omit it and the SDK falls back to the table renderer. */ view_type?: 'table' | 'kanban' | (string & {}) /** * Column key the board groups by when `view_type === 'kanban'` (the stage * column, e.g. `'stage'`). Each distinct value of this column becomes a board * lane. Mirrors the nav item's `group_by` (RFC §1.2). */ group_by?: string /** * Board lanes (the stage machine of the `group_by`/`stage_field` column). * When present the kanban renders one lane per stage in `order`. When absent * the SDK derives lanes from the `group_by` column's `options` (the kernel * already projects `stages[]` onto the status display — RFC §1.1). Snake_case * keys as the kernel serves them. */ stages?: StageMeta[] /** * Virtual "smart" lanes (ops #704): read-only board columns defined by a set * of filters rather than a stored stage value. When present the kanban paints * one lane per entry (querying the list with the lane's filters) after the * real stages. Purely additive — absent on hosts without custom stages. */ smart_lanes?: SmartLaneMeta[] /** * Allowed stage transitions (RFC §1.1). When present, the kanban only lets a * card drop into a lane reachable from its current stage; disallowed lanes * are dimmed and reject the drop. `from`/`to` accept `'*'` as a wildcard. * Absent → any move is allowed (the kernel still validates server-side). */ transitions?: StageTransition[] } /** * One board lane / pipeline stage. Mirrors the kernel v3 `Stage` (RFC §1.1). * `color` is a semantic palette name (`'slate'`, `'blue'`, `'amber'`, `'green'`) * or a hex literal — resolved through the same `generateBadgeStyles` helper as * option badges. `is_final` flags a terminal stage (e.g. "Done"). */ export interface StageMeta { key: string label: string color?: string order?: number is_final?: boolean /** * True when the kernel merged a user-defined custom stage into `stages[]` * (ops #704). Behaves as a normal droppable lane; the SDK just grows an * Editar/Eliminar menu on it. */ custom?: boolean /** * True when a per-org stage override (label/color/conditions) has been * applied to this DECLARED lane (ops stage-overrides). The kernel serves the * lane already carrying the overridden label/color; this flag only drives the * "Restablecer etapa" affordance in the config dialog. Absent on hosts without * stage overrides — purely additive. */ overridden?: boolean /** * Extra per-lane conditions layered on top of the stage's own `group_by` * scope (ops stage-overrides). When present the lane queries its data — and * counts its header — with the stage filter PLUS these conditions (serialized * the same way as smart-lane filters). The lane stays a normal drop target; * dropping a card only sets the stage value. Absent → the lane behaves as a * plain declared stage. Snake_case ops as the kernel serves them. */ filters?: { field: string; op: string; value: string }[] /** * The manifest ORIGINAL (pre-override) label/color/conditions, served * alongside an overridden declared lane so the "Restablecer al original" * confirm can spell out exactly what reverts. Optional — hosts that don't * snapshot the original simply omit it and the SDK shows a generic confirm. */ original?: { label?: string color?: string filters?: { field: string; op: string; value: string }[] } } /** * A virtual "smart" lane (ops #704) served in `TableMetadata.smart_lanes`. It's * defined by `filters` (never a stored stage value), so the board paints it by * querying the list with those conditions. Read-only — not a drop target. */ export interface SmartLaneMeta { key: string label: string color?: string order?: number filters: { field: string; op: string; value: string }[] } /** Allowed `from → to` stage transition (RFC §1.1). `'*'` is a wildcard. */ export interface StageTransition { from: string to: string } /** * Describes one child relation of a parent model, mirroring the kernel * `RelationMeta` shape (>= v0.41.0). Drives the metadata-driven * `` panel list. All keys are snake_case as served by the * kernel; the SDK reads them as-is. */ export interface RelationMeta { /** Stable identifier for the relation (used as a React key / data attr). */ name: string /** Cardinality. The SDK maps this onto `DynamicRelation.kind`. */ kind: 'one_to_many' | 'many_to_many' /** * Child model key (the `through` model). For one_to_many this is the model * whose rows are listed; for many_to_many it is the pivot table. */ through: string /** Child column holding the FK back to the parent. */ foreign_key: string /** * Static equality filters applied on top of the foreign-key scope. Used for * polymorphic children (e.g. `{ "owner_model": "Customer" }`) so a shared * attachments/addresses table is narrowed to this parent's rows. Each entry * becomes a `f_=eq:` query param. */ scope?: Record /** Human-readable panel header. */ label?: string /** * Read-only relation: when true the SDK forces canCreate/canEdit/canDelete * = false on the rendered `` panel, hiding the "Agregar" * button and the per-row edit (Pencil) / delete (Trash2) controls — no * matter what perms the host passes. Generic & domain-agnostic. Read from * the kernel relation metadata. Tolerates the camelCase alias. */ readonly?: boolean /** camelCase alias for `readonly`. */ readOnly?: boolean /** * Composition relation: its children are PART of the parent record (a * document's lines), so the record MODAL embeds them as a sub-table. * Opt-in — a modal renders only the relations carrying this flag, which is * what keeps opening a warehouse from loading every stock movement it has. * Served by the kernel from the manifest relation's `embed`. Absent (older * kernels) = not embedded. */ embed?: boolean } export interface FilterDefinition { key: string label: string /** * `dynamic_select` resolves its options server-side from a relation * (`searchEndpoint = /options/`) and renders the same multi-value * combobox as `select`. The host loads + caches the options before they * surface in the dropdown. */ type: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text' column: string options?: { value: string | boolean; label: string; icon?: string; color?: string }[] searchEndpoint?: string } /** * Where a column is rendered. Mirrors `manifest.ColumnDef.Visibility` in the * kernel: * - `''` / `'all'` — visible everywhere (default). * - `'table'` — only the list/index page. * - `'modal'` — only the create/edit modal. * - `'list'` — only API list payloads (omitted from UI). * Hosts may extend the union with their own scopes; the SDK only acts on the * canonical values above. */ export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {}) export interface ColumnDefinition { key: string label: string type: | 'text' | 'number' | 'date' // Timestamp variants. They share the `date` cell renderer but append // the time + a full-precision tooltip (see formatDateCell). | 'datetime' | 'timestamp' | 'timestamptz' | 'select' | 'search' | 'relation-badge-list' | 'avatar' | 'boolean' | 'phone' | 'media-gallery' | 'image' // Landscape stack: wide image on top, label underneath (logos/photos). | 'image_stack' // Declarative pro cell renderers (resolved via `cellStyle ?? type`). | 'url' | 'link' | 'email' | 'currency' | 'percent' | 'progress' | 'badge' | 'status' | 'tags' | 'color' | 'code' | 'truncate-text' | 'creator' | 'user' // Resolved FK relation chip. The data row carries a sibling // `{ value, label }` object keyed by the column key with the trailing // `_id` stripped (e.g. `category_id` → `row.category`). Also triggered // implicitly whenever the column carries a `ref` (belongs_to FK). | 'relation' sortable: boolean filterable: boolean /** * Explicit filter UI the backend wants for this column when `filterable`. * When absent the SDK infers it from the column shape (options/endpoint → * `select`, boolean/number/date → their range pickers, else `text`). A * `ref` (belongs_to FK) column is served as `dynamic_select` so its options * stream from `searchEndpoint = /options/` into a multi-value combobox. */ filterType?: 'select' | 'dynamic_select' | 'boolean' | 'date_range' | 'number_range' | 'text' hidden?: boolean /** * Scopes where this column is rendered. When `'modal'` (or `'list'`) the * column is hidden from the table even if `hidden` is unset. Empty/`'all'`/ * `'table'` keep the column visible. See `column-visibility.ts`. */ visibility?: ColumnVisibility /** * Opts the column into the model's full-text/contains search. Independent * of `filterable` (which drives column-level filter chips). When at least * one column declares `searchable`, the SDK narrows the global search to * those columns; otherwise legacy "search every column" behaviour applies. */ searchable?: boolean styleConfig?: Record tooltip?: string description?: string cellStyle?: string searchEndpoint?: string filterField?: string basePath?: string displayField?: string iconField?: string relationPath?: string useOptions?: boolean options?: { value: string; label: string; icon?: string; color?: string }[] /** * FK target model. When the kernel auto-derives this from a * belongs_to relation (or an author sets it explicitly), the SDK * resolves the column's options against `/api/options/?field=id` * via `useOptionsResolver`. Wins over `searchEndpoint` for select * widgets — `searchEndpoint` stays as the legacy escape hatch. */ ref?: string /** * Write-time rules the SDK also pre-flights. Object form `{regex,min,max,custom}` * or a Laravel / go-playground string (`required|min:2|email`). */ validation?: FieldValidation | string /** * Declared schema for a jsonb line-items column (kernel v3 `item_fields`). * Each entry describes one sub-field of the array's row objects: a `key` * (the jsonb key), an already-LOCALIZED `label` (backend-translated), an * optional `type` hint and an optional `ref` (FK target). When present the * `CollectionCell` renders the popover mini-table with these headers in * order and resolves `ref` columns to the backend-injected sibling label * (the FK key without `_id`, else `_label`) instead of the raw uuid. * Tolerates the snake_case `item_fields` the kernel serves. */ itemFields?: ColumnItemField[] /** snake_case alias served by the kernel for `itemFields`. */ item_fields?: ColumnItemField[] /** * Conditional visibility (kernel v3 `Column.visible_when`): * - create/edit modal → `evaluateVisibleWhen` against live form values * - list/board → `evaluateVisibleWhenForListScope` against known filter * scope (`defaultFilters` / single-eq chips), so a locked nav scope * like `party_type=customer` hides `supplier_id` without each nav * item re-declaring a full column allowlist. * Tolerates the camelCase alias. Absent = always visible. */ visible_when?: VisibleWhen /** camelCase alias for `visible_when`. */ visibleWhen?: VisibleWhen /** * Form-layout membership: the key of the `form_layout` section this column's * modal field belongs to (kernel PR #230). Absent → the default group. See * `groupFieldsBySection`. */ section?: string } /** * One declared sub-field of a jsonb line-items column (see * `ColumnDefinition.itemFields`). `label` is already localized by the backend * and consumed verbatim; a non-empty `ref` flags the column for resolved-label * rendering against the injected sibling. Structurally compatible with the * `ItemField` consumed by `collection-cell`. */ export interface ColumnItemField { key: string label: string type?: string ref?: string } export interface ActionCondition { field: string operator: 'eq' | 'neq' | 'in' | 'not_in' value: string | string[] } /** * Conditional-visibility predicate for a create/edit form field. Mirrors the * kernel v3 `visible_when` block (json `visible_when`): the owning field is * rendered only when the SIBLING field named by `field` holds a value that * matches — either equal to `equals` (exact string) OR a member of `in` * (any-of). When both are set `in` wins. Absent = the field is always visible * (retrocompat). Evaluated by `evaluateVisibleWhen` against the live form * values; a hidden field never gates submit (its required-check is skipped). */ export interface VisibleWhen { field: string equals?: string in?: string[] } // Write-time + client-side constraints. The kernel enforces these on // create/update and action payloads (locale-agnostic codes); the SDK // pre-flights the same rules and localizes `validation.` to the // operator's language. `custom` is a slug (`email`, `rfc.tax_id`) or a // `$org.` reference resolved through OrgConfigProvider. export interface FieldValidation { regex?: string min?: number max?: number custom?: string } // Widget hints for the form renderer. Subset that DynamicForm knows how to // render today; unknown values fall back to the `type`-based default. export type FieldWidget = | 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch' | 'upload' | 'icon' /** * Per-option visibility gate for a STATIC enum (`options[]`). The option is only * offered when the value of a sibling field (`field`, defaulting to the * containing field's `dependsOn`) passes: value ∈ `in` and value ∉ `notIn`. * Comparison is by string. Mirrors the kernel v3 `option.when` block; the SDK * tolerates both the snake_case (`not_in`) the kernel serves and camelCase. * An option WITHOUT a `when` always applies (retrocompat). */ export interface OptionWhen { /** Sibling field whose value gates this option. Falls back to `dependsOn`. */ field?: string /** The option applies when the gating field's value is in this list. */ in?: string[] /** The option applies when the gating field's value is NOT in this list. */ notIn?: string[] /** snake_case alias served by the kernel manifest for `notIn`. */ not_in?: string[] } /** * A single static enum option. `value`/`label` are the core pair; `icon`, * `color` and `image` drive the option's leading visual where the renderer * supports it. `when` gates the option's visibility by a sibling field's value * (see {@link OptionWhen}) — used for dependent/cascading STATIC enums. */ export interface OptionDef { value: string label: string icon?: string color?: string image?: string when?: OptionWhen } export interface ActionFieldDef { key: string label: string type: string required?: boolean /** * Explicit nullability flag served by the kernel (v0.77.1+) from * `modelbase.FieldDef.Nullable` (populated as `!Required`). An optional `ref` * arrives as `nullable: true`. When present it authoritatively decides whether * an empty value should be submitted as `null`; when absent (older hosts) the * SDK falls back to type-based heuristics. Additive / optional. */ nullable?: boolean options?: OptionDef[] defaultValue?: any placeholder?: string searchEndpoint?: string validation?: FieldValidation | string widget?: FieldWidget | string /** * FK target model — same semantics as ColumnDefinition.ref. When * present, DynamicForm resolves the field's options through * `useOptionsResolver` against `/api/options/?field=id`. */ ref?: string /** * snake_case aliases the kernel manifest may serve for a belongs_to FK * target instead of `ref`. Treated as equivalent to `ref` by the SDK so a * declared relation renders a searchable picker regardless of which key the * backend emits. */ source?: string relation?: string /** * A field with a declared `ref` (or `source`/`relation`) normally renders * as a single-value searchable picker (`dynamic_select`). Set `multiple: * true` to render `DynamicMultiSelectField` instead, storing the value as * a plain array of target ids — the backing column must be array-shaped * (a jsonb column, the kernel's canonical choice). E.g. a price list that * applies to several customer segments at once rather than exactly one. */ multiple?: boolean /** * Cascade dependency: the key of ANOTHER field in the same action form * (a header field or a sibling item-field) whose current value supplies * this picker's `filter_value`. While the depended-on field is empty the * picker is disabled with a hint; once it has a value the picker fetches * options scoped by it and re-fetches whenever it changes (clearing the * current selection). Without `dependsOn` the picker lists everything * (retrocompat). Tolerates the snake_case `depends_on` the kernel serves. */ dependsOn?: string /** snake_case alias served by the kernel manifest for `dependsOn`. */ depends_on?: string /** * Enriched options routing the kernel serves for a dependent/scoped picker. * When it carries a `source`, the picker queries that source MODEL (not the * field's `ref`): URL `/options/`, query field = `value` (falling * back to the field's own key), and the cascade `filter_value` is the value * of the `dependsOn` field. `description` is projected into the option * subtitle. Tolerates the snake_case `options_config` the kernel emits. * Absent → the picker keeps its `ref`-based behaviour (retrocompat). */ optionsConfig?: FieldOptionsConfig /** snake_case alias served by the kernel manifest for `optionsConfig`. */ options_config?: FieldOptionsConfig /** * Opt-in: ofrecer escaneo por cámara para LLENAR RÁPIDO este campo. Cuando * es `true` y el navegador soporta `BarcodeDetector`, el picker searchable * muestra un botón de cámara: el código escaneado alimenta la búsqueda para * elegir la referencia sin tipear el UUID/SKU (ideal para renglones de * orden de compra, variantes de producto, etc.). País/negocio-agnóstico. * Additive / opcional; los hosts viejos lo ignoran. Tolera `scannable`. */ scan?: boolean /** Alias tolerado para `scan`. */ scannable?: boolean /** * Columns of a repeatable line-items group. Mirrors the kernel v3 * `ActionField.item_fields` (json `item_fields`). Present on a field * with `type: "array"` — the multi-row container (e.g. the item rows * of a "Recibir mercancía" modal, or the debit/credit lines of a * journal entry). Each entry is itself an ActionFieldDef describing * one column's cell widget. The field value is an array of objects * keyed by these item field keys. Rendered by `DynamicLineItems`. */ itemFields?: ActionFieldDef[] /** * On an `itemFields` column: flags the column for summation in the * line-items footer. The SDK renders a totals row summing every numeric * column marked `total` (e.g. the debit and credit columns of a journal * entry). Ignored on flat fields. Mirrors kernel v3 `ActionField.total`. */ total?: boolean /** * On a line-items (`type: "array"`) field: declares an optional, generic * balance constraint between two summed columns. The SDK shows a balanced / * out-of-balance indicator and blocks submit until the two sides match. * Domain-agnostic — "debit"/"credit" are just the two column keys to * reconcile. Mirrors kernel v3 `ActionField.balance`. */ balance?: FieldBalanceRule /** * On a line-items (`type: "array"`) field: fixes the set of rows. When true * the SDK hides the "Agregar renglón" button and the per-row delete control, * so the rows served (e.g. a prefilled set of allocations) can only have * their cells edited — never added to or removed. Generic & domain-agnostic. * Mirrors kernel v3 `ActionField.lock_rows`. Tolerates the camelCase alias. */ lock_rows?: boolean /** camelCase alias for `lock_rows`. */ lockRows?: boolean /** * `upload` widget: comma-separated accept list forwarded to the file input * `accept` attribute (e.g. `"image/*,.pdf"`). Tolerates the snake_case the * kernel may serve. Optional — when absent any file type is allowed. */ accept?: string /** * `upload` widget: maximum file size in bytes. The renderer rejects larger * files client-side before POSTing. Tolerates kernel snake_case `max_size`. */ maxSize?: number /** snake_case alias served by the kernel manifest for `maxSize`. */ max_size?: number /** * `upload` widget: server-side storage bucket/prefix the host writes the * file under, forwarded to the upload endpoint as `storage_path`. Tolerates * kernel snake_case `storage_path`. */ storagePath?: string /** snake_case alias served by the kernel manifest for `storagePath`. */ storage_path?: string /** * Conditional visibility: render this field only when a sibling field's * current value matches the predicate. Mirrors the kernel v3 * `ActionField.visible_when`. Tolerates the camelCase alias. Absent = always * visible; a hidden field is skipped by the required-gate so it never blocks * submit. See `evaluateVisibleWhen`. */ 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 field lands in the default * group. See `groupFieldsBySection`. */ section?: string } /** * Declarative reconciliation constraint on a line-items field: the summed value * of `debitColumn` across all rows must equal the summed value of * `creditColumn`. Tolerates the snake_case shape the kernel serves * (`debit_column` / `credit_column` / `require_nonzero`). Generic by design. */ export interface FieldBalanceRule { debitColumn?: string creditColumn?: string /** snake_case alias served by the kernel manifest. */ debit_column?: string /** snake_case alias served by the kernel manifest. */ credit_column?: string message?: string /** When true (default) an all-zero entry is treated as out of balance. */ requireNonzero?: boolean require_nonzero?: boolean } /** * Enriched options-resolution config the kernel attaches to a dependent/scoped * picker field (json `options_config`). When `source` is present the SDK queries * the source model instead of the field's `ref`. All keys are snake_case as the * kernel serves them; the SDK reads them as-is via `getOptionsConfig`. */ export interface FieldOptionsConfig { /** Discriminator the kernel sets (e.g. `'dynamic'`). Informational. */ type?: string /** Source MODEL the candidates come from → URL `/options/`. */ source?: string /** Column of `source` compared against the cascade `filter_value`. */ filter_by?: string /** Column of `source` used as the option value → query `?field=`. */ value?: string /** Related model used to resolve the option label by id (host-side enrich). */ label_ref?: string /** Column of `source` projected into `option.description` (e.g. qty). */ description?: string /** Optional ordering column. */ order_by?: string /** Optional column projected into `option.image`. */ image?: string } export interface ActionDefinition { key: string name: string label: string icon: string class: string color?: string type: 'view' | 'edit' | 'delete' | 'custom' | 'link' linkUrl?: string condition?: ActionCondition confirm?: boolean confirmMessage?: string fields?: ActionFieldDef[] /** Multi-step wizard form; when present the dispatcher renders a wizard. */ steps?: ActionStep[] requiresState?: string[] executable?: boolean /** * Where the host surfaces the trigger. Mirrors manifest/v3 Action.placement. * "row" (default) — per-row table action. * "table" — page toolbar button (no record context). * "create" — toolbar button that replaces the generic create button. */ placement?: 'row' | 'table' | 'create' } export interface ApiResponse { success: boolean data: T meta?: PaginationMeta filters?: Record message?: string } export interface PaginationMeta { current_page: number from: number last_page: number per_page: number to: number total: number } // ActionMetadata re-exported from the sdk's action-registry. We mirror the // subset needed for the dispatcher so consumers of runtime-react don't have to // import the sdk directly for prop typings. /** One page of a multi-step (wizard) action. Mirrors the sdk's ActionStep. */ export interface ActionStep { title: string description?: string fields: ActionFieldDef[] } export interface ActionMetadata { key: string label: string icon: string color?: string confirm?: boolean confirmMessage?: string fields?: ActionFieldDef[] /** Multi-step wizard form; when present the dispatcher renders a wizard. */ steps?: ActionStep[] requiresState?: string[] executable?: boolean placement?: 'row' | 'table' | 'create' }