/** * Definition types for @siesa/master-pattern-view * * These types define the structure and configuration of a master view. * They are the "single source of truth" that drives all UI behavior. * * @module definition.types * @see FR-001, FR-002, FR-003 */ import type React from 'react'; import type { Fetcher } from '../../LookupField/services/api.types'; import type { SimpleFilter, FilterExpression } from '../../LookupField/types/filter.types'; import type { LookupFieldCreateHelpers } from '../../LookupField/LookupField.types'; import type { MasterPatternViewEntity, MasterPatternViewService } from './service.types'; import type { MasterPatternViewPermissions } from './permissions.types'; /** * Input type for form fields. * When not specified, the component infers the input type from `dataType`. * * @see FR-003 — Field type inference table */ export type MasterPatternViewInputType = 'text' | 'number' | 'decimal' | 'boolean' | 'date' | 'select' | 'lookup' | 'textarea' | 'email' | 'password' | 'file'; /** * Filter operators for advanced filtering. * * @see FR-005 — Advanced filter panel */ export type FilterOperator = 'equals' | 'not_equals' | 'contains' | 'starts_with' | 'ends_with' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'between'; /** * Option for select-type fields (static dropdown). * * @param value - The value stored in the entity * @param label - The display label shown to the user */ export interface SelectOption { value: string | number; label: string; } /** * Configuration for lookup-type fields (remote search via siesa-ui-kit LookupField). * Maps 1:1 to LookupFieldProps so consumers can pass config through without adapter code. * * @see FR-003 — LookupField integration */ export interface MasterPatternViewLookupConfig { /** Entity name for the API endpoint (e.g. 'economic-activities', 'customers') */ entity: string; /** Fetcher function that performs the HTTP request */ fetcher: Fetcher; /** Fields to display in the dropdown options */ displayFields: string[]; /** * Template string with {field} placeholders for the selected value display. * Example: "{code} - {name}" → "C001 - Acme" * Priority: displayTemplate > displayValue > all displayFields joined */ displayTemplate?: string; /** Single field to show in the trigger for the selected record */ displayValue?: string; /** * Extra fields to fetch alongside `Id` and `displayFields`, for use in `onFormSelect`/ * `onFilterSelect` cascades — e.g. a field not shown in the dropdown but needed to * auto-fill or enable a sibling field. Maps 1:1 to LookupField's own `bindFields`. * Without this, a cascade reading `record.CountryID` (say) always sees `undefined`, * because the lookup never requested that column — whether the record was picked from * search or created inline via `create`. * * @example * lookupConfig: { * entity: 'departments', * displayFields: ['code', 'name'], * bindFields: ['CountryID'], * onFormSelect: (record, setFormValue) => { * if (record?.CountryID) setFormValue('countryId', record.CountryID); * }, * } */ bindFields?: string[]; /** Static or expressive filters applied to all requests */ filters?: SimpleFilter | FilterExpression; /** * Dynamic filter callback — receives the current filter panel values and returns * a `SimpleFilter` to apply to lookup requests. * Use this for **forward cascade**: e.g., show only departments that belong to the * currently selected country. * * Takes precedence over `filters` when both are provided. * * @param filterValues - Snapshot of all basic-filter field values, keyed by `fieldName`. * @returns A `SimpleFilter` object (may be empty `{}` when no restriction applies). * * @example * // Filter departments by the currently selected country * getFilters: (filterValues) => * filterValues.countryId * ? { CountryId: filterValues.countryId as string } * : {}, */ getFilters?: (filterValues: Record) => SimpleFilter; /** * Fired after the user selects a record in the filter lookup. * Use this for **reverse cascade**: e.g., auto-fill the country filter when a * department is selected. * * Only called in basic-filter mode (not in the advanced-filter panel). * * @param record - The full selected record returned by the lookup. * @param setFilterValue - Call with `(fieldName, value)` to update a sibling filter field. * * @example * // Auto-fill country when a department is picked * onFilterSelect: (record, setFilterValue) => { * if (record.CountryID) setFilterValue('countryId', record.CountryID); * }, */ onFilterSelect?: (record: Record, setFilterValue: (fieldName: string, value: unknown) => void) => void; /** * Fired after the user selects or clears a record in the **form** lookup. * Use this for **reverse cascade in the form**: e.g., auto-fill a parent field when a * child record is selected, or clear a dependent field when the parent is cleared. * * @param record - The full selected record, or `null` when the field is cleared. * @param setFormValue - Call with `(fieldName, value)` to update a sibling form field. * @param context - Extra context for the change: * - `previousRecord`: the record that was selected before this change (or `null`). * - `revert()`: call to undo the change and restore the previous value in the form field. * * @example — Confirm before accepting a change * onFormSelect: (record, setFormValue, { previousRecord, revert }) => { * if (hasPendingData && record?.Id !== previousRecord?.Id) { * showConfirmDialog({ onCancel: revert }); * } * }, */ onFormSelect?: (record: Record | null, setFormValue: (fieldName: string, value: unknown) => void, context: { previousRecord: Record | null; revert: () => void; }) => void; /** Minimum characters before search triggers (default: 1) */ minChars?: number; /** * Turns this lookup into an "entity control": a "+" button next to the trigger * opens a popup hosting a `MasterPatternViewCreateForm` for `definition`/`service` — * no custom form code needed. On success the new record is selected automatically, * exactly like `LookupField`'s own `onCreateNew` prop, which this maps onto internally. * * The record `MasterPatternViewService.create()` resolves with is rarely shaped * exactly like this lookup's own `entity` `fetcher` (search) response — e.g. a * reverse-cascade `onFormSelect` reading `rec.CountryID`/`rec.StateID` needs fields * the search fetcher injects that a plain create response never has. To keep the * auto-selected record consistent with one picked from the dropdown, the created * record is re-fetched by `Id` through this same `fetcher` right after creation * (same fields as a normal search: `Id` + `displayFields`) before `onFormSelect`/ * the trigger ever see it — falling back to the raw (Id-bridged) create response * only if that re-fetch itself fails (e.g. read-after-write lag). * * @example * // A "Cliente" lookup that can also create a new customer inline * lookupConfig: { * entity: 'customers', * fetcher, * displayFields: ['code', 'name'], * create: { * useDefinition: () => customerDefinition, * service: customerService, * icon: , * }, * } */ create?: { /** * Returns the related master's definition — same shape its own `` * uses. This is a FUNCTION, not a resolved value, so it can be a parameterless React * hook (e.g. `useAccountDefinition`) without the caller having to invoke it outside * of a render. That matters specifically for a SELF-REFERENCING lookup — a field * that points back at the SAME entity this `lookupConfig` belongs to (e.g. Account's * own "parent account" field). If the definition had to be resolved eagerly to build * this config, building Account's own definition would require calling * `useAccountDefinition()` from inside `useAccountDefinition()` itself — direct, * unconditional recursion that crashes the very first render, not a rules-of-hooks * nuance to work around. Passing the function instead defers the call until * `MasterPatternViewCreateForm` actually mounts, which only happens when the user * opens the "+" popup — a fresh, independent render, not a call nested inside the * definition that's still being constructed. * * For a definition that's already a plain constant (no hook involved), just wrap it: * `useDefinition: () => customerDefinition`. * * Same-repo only: this requires importing the target master's real * `MasterPatternViewDefinition`/`MasterPatternViewService`, which only works when the * target lives in the same TypeScript build. For a lookup pointing at a master that * lives in a DIFFERENT repo (a separate MFE — the two can't import each other's * modules), use `onCreateNew` below instead; omit `useDefinition`/`service` in that case. */ useDefinition?: () => MasterPatternViewDefinition; /** The related master's service. Required unless `onCreateNew` is provided instead. */ service?: MasterPatternViewService; /** * Cross-repo / fully custom escape hatch — same contract as ``'s own * `onCreateNew`: render whatever create UI you want (it doesn't have to be a * `MasterPatternViewCreateForm` at all), call `onCreated(record)` when it succeeds * (the lookup selects it automatically) or `onCancel()` to close without changing the * selection. Use this when the target master lives in a different repo/MFE, so its * real `useDefinition`/`service` can't be imported here. When provided, it takes * priority over `useDefinition`/`service` (which can be omitted entirely). */ onCreateNew?: (helpers: LookupFieldCreateHelpers) => React.ReactNode; /** Human-readable entity name for the popup header. Falls back to `definition.name`. */ entityName?: string; /** * Popup shell style. * - `'modal'` — centered modal with overlay (default) * - `'sidebar'` — slide-over panel from the right edge * @default 'modal' */ variant?: 'modal' | 'sidebar'; /** Popup header title override. Falls back to `entityName`. */ title?: string; /** aria-label / tooltip text for the "+" button. Falls back to a generic "Crear nuevo". */ buttonLabel?: string; /** * Custom popup width — relative units only (`vw`, `%`, `min(...)`), never `px`. * Useful for a master with several tabs that needs real room. Omit for the * compact default. */ width?: string; /** * Maximum popup height — same rules as `width`. Omit for the compact default. Always * a CAP, never a fixed height — the panel shrinks to its content and only grows up to * this value. */ height?: string; /** Optional decorative icon shown in a badge next to the popup title. */ icon?: React.ReactNode; /** * Whether the "+" button is actually rendered. Set to `false` to keep `create` * fully configured while hiding the affordance (e.g. gating by permission). * @default true */ showCreateButton?: boolean; /** * Whether the popup renders its own header (icon + title + close button) — same * contract as ``'s own `createShowHeader`. Auto-computed when omitted: * `false` for the `useDefinition`/`service` path (MasterPatternViewCreateForm always * renders its own complete header — a second one would just duplicate it) and left as * the popup's default (`true`) for the `onCreateNew` escape hatch, since that content * typically has no header of its own. Set explicitly to override either case — e.g. * `false` for an `onCreateNew` render function that, like MasterPatternViewCreateForm, * already renders its own header. */ showHeader?: boolean; /** * Permissions for the CHILD entity's create form — deliberately separate from the * `permissions` prop on the master that HOSTS this lookup. Example: a "Compañía" form * has a "Teléfono" lookup with `create` enabled — whether the user can even use/search * the lookup is governed by Compañía's own permissions (the "parent" form they're * already inside), but whether the "+" button may create a new Teléfono record must * check Teléfono's OWN `canCreate`, not Compañía's. Pass the target entity's actual * permission set here; omit to leave the "+" ungated (matches today's behavior). */ permissions?: MasterPatternViewPermissions; }; } /** * Configuration for file-type fields (upload via siesa-ui-kit FileUploader). * * @see FR-003 — File field integration */ export interface MasterPatternViewFileConfig { /** * Function that performs the upload. * Receives the File object and a progress callback (0-100). * Must return a Promise resolving to the file URL or an object with `url`. */ uploadFunction: (file: File, onProgress: (progress: number) => void) => Promise; /** Accepted MIME types / extensions (e.g. `{ 'image/*': ['.png', '.jpg'] }` or `".pdf,.docx"`) */ accept?: Record | string; /** Maximum file size in bytes (default: 5 MB) */ maxSize?: number; /** Allow multiple file selection (default: true) */ multiple?: boolean; /** Visual variant of the uploader (default: 'dropzone') */ variant?: 'dropzone' | 'button' | 'minimal'; /** Upload immediately on file selection (default: true) */ autoUpload?: boolean; } /** * Tab configuration for organizing form fields into tabs. * * @see FR-007 — Form tabs */ export interface MasterPatternViewTabConfig { /** Unique key for the tab */ key: string; /** Display label for the tab */ label: string; /** Tab order (lower = first) */ order?: number; /** Sections within this tab */ sections?: MasterPatternViewSectionConfig[]; /** * Conditionally show or hide this tab based on the current form values. * Evaluated on every form change. When `false`, the tab is removed from the tab bar * and the active tab resets to "Datos básicos" if it was the hidden one. * * @example * // Show the "Addresses" tab only when the entity type is 'company' * visible: (values) => values.entityType === 'company' */ visible?: (values: Record) => boolean; } /** * Section configuration for grouping fields within a tab. * * @see FR-007 — Form sections */ export interface MasterPatternViewSectionConfig { /** Unique key for the section */ key: string; /** Display label for the section */ label: string; /** Section order within the tab */ order?: number; /** Number of columns in the section grid */ columns?: number; } /** * Field definition that drives all UI behavior. * Each field in the definition array configures one column/input/filter. * * @see FR-003 — Field definitions drive all UI * * @param fieldName - Property name in the entity (e.g., 'code', 'name') * @param label - Display label for the field * @param dataType - Data type of the field value * @param inputType - Explicit input type override (auto-inferred from dataType if not set) * @param isRequired - Whether the field is required in forms * @param maxLength - Maximum character length for string fields * @param isOverridable - Whether this field can be overridden per company (GLOBAL type only). * Mutually exclusive with isCompanyOnly at the business level — enforced at runtime, not type level. * @param isCompanyOnly - Whether this field exists only in company context (GLOBAL type only). * Mutually exclusive with isOverridable at the business level — enforced at runtime, not type level. * @param showInList - Whether to show this field as a column in the list table * @param showInForm - Whether to show this field in the form * @param quickFilter - Whether to show as an inline quick filter in the toolbar * @param sortable - Whether the column is sortable (code and name are always sortable) * @param filterable - Whether to include in the advanced filter panel * @param listOrder - Column position in the list table (lower = left). Defaults to array declaration order. * @param formTab - Key of the tab this field belongs to * @param formSection - Key of the section this field belongs to * @param formOrder - Field position within its form section (lower = first). Independent of listOrder. * @param colSize - Responsive column size for form layout * @param options - Static options for select-type fields * @param lookupConfig - Configuration for lookup-type fields * @param renderCell - Custom render function for list column cell */ export interface MasterPatternViewFieldDefinition { fieldName: string; label: string; dataType: 'string' | 'number' | 'decimal' | 'boolean' | 'date' | 'guid'; inputType?: MasterPatternViewInputType; isRequired?: boolean; maxLength?: number; isOverridable?: boolean; isCompanyOnly?: boolean; showInList?: boolean; showInForm?: boolean; quickFilter?: boolean; sortable?: boolean; filterable?: boolean; /** Column position in the list table. Defaults to array declaration order. */ listOrder?: number; formTab?: string; formSection?: string; /** Field position within its form section. Independent of listOrder. */ formOrder?: number; colSize?: number | { sm?: number; md?: number; lg?: number; }; /** Number of grid rows to span. Useful for tall fields (e.g. file dropzone) beside short inputs. */ rowSpan?: number; options?: SelectOption[]; lookupConfig?: MasterPatternViewLookupConfig; fileConfig?: MasterPatternViewFileConfig; /** Custom render function for list column cell */ renderCell?: (value: unknown, row: unknown) => unknown; /** * Position of this field within each card in the Cards view. * - `'title'` — large title at the top of the card (first `title` field wins) * - `'subtitle'` — secondary text below the title * - `'body'` — shown in the main content area (default when not specified) * - `'footer'` — shown at the bottom of the card * If no field has a `cardPosition`, the first `showInList` field becomes the title * and the rest are shown in the body. */ cardPosition?: 'title' | 'subtitle' | 'body' | 'footer'; /** * Whether this field is shown in the Cards view. * Defaults to the same value as `showInList` when not specified. */ showInCard?: boolean; /** * Custom render function for the form field. * Replaces the auto-generated input entirely. * Receives the field's current value, its onChange handler, all form values, * and a context object with the current readOnly flag and form mode. * The label wrapper and error slot are still rendered by FieldRenderer. * * @param value - Current value of this field * @param onChange - Call with the new value to update the field * @param allValues - All current form values (read-only snapshot) * @param context - `{ readOnly, mode }` — use to disable inputs in view mode or * adjust rendering between create / edit modes. Existing 3-arg signatures * continue to work unchanged. * * @example * renderFormField: (value, onChange, allValues, { readOnly, mode }) => ( * * ) */ renderFormField?: (value: unknown, onChange: (value: unknown) => void, allValues: Record, context: { readOnly: boolean; mode: 'create' | 'edit' | 'view'; }) => React.ReactNode; /** * Disables the field in the form. * - `boolean` — disables the field statically * - `(allValues) => boolean` — disables the field dynamically based on current form values * * @example * // Static: always disabled * disabled: true * * @example * // Dynamic: disabled until countryId has a value * disabled: (values) => !values.countryId */ disabled?: boolean | ((allValues: Record) => boolean); } /** * Form section — groups fields under a titled heading in the Datos básicos form. * Sections are rendered in declaration order; each section gets its own grid row. * * @param label - Optional title shown above the section. Omit for an untitled section. * @param icon - Optional lucide-style icon rendered to the left of the label. * @param fields - Fields belonging to this section. */ export interface MasterPatternViewFormSection { label?: string; icon?: React.ComponentType<{ size?: number; className?: string; }>; fields: MasterPatternViewFieldDefinition[]; } /** * Master view definition — the single source of truth for a master entity's UI. * Mirrors the C# MasterDefinition concept on the frontend. * * Use either `sections` (structured form with titled groups) or `fields` (flat, no titles). * At least one must be provided. Both can coexist — `sections` is used for form rendering * while `fields` can be provided for list-only fields not shown in any form section. * * @see FR-001 — MasterPatternViewDefinition is the core config * @see FR-002 — type determines MasterType behavioral contracts * * @example — with sections (titled groups) * ```typescript * const accountDefinition: MasterPatternViewDefinition = { * name: 'Account', * type: 'GLOBAL', * sections: [ * { label: 'Identificación', icon: Fingerprint, fields: [ * { fieldName: 'code', label: 'Código', dataType: 'string', isRequired: true }, * { fieldName: 'name', label: 'Nombre', dataType: 'string', isRequired: true }, * ]}, * ], * }; * ``` * * @example — flat (backward compatible) * ```typescript * const currencyDefinition: MasterPatternViewDefinition = { * name: 'Currency', * type: 'GLOBAL', * fields: [ * { fieldName: 'code', label: 'Code', dataType: 'string', isRequired: true }, * { fieldName: 'name', label: 'Name', dataType: 'string', isRequired: true }, * ], * }; * ``` */ export interface MasterPatternViewDefinition { /** Unique name identifier for the master — used as DevTools label */ name: string; /** * MasterType: * - `GLOBAL` — single global dataset with optional per-company overrides * - `UNIVERSAL` — single global dataset, no company context * - `COMPANY_SPECIFIC` — each company manages its own independent records * - `CONFIG_TABLE` — bulk config table: rows from external source, inline editing, bulk save. Requires `companyBehavior`. * - `CUSTOM` — shell only (toolbar + company context). Requires `companyBehavior` and `renderBody` prop. */ type: 'GLOBAL' | 'UNIVERSAL' | 'COMPANY_SPECIFIC' | 'CONFIG_TABLE' | 'CUSTOM'; /** * Company behavior for CONFIG_TABLE and CUSTOM types. * Ignored for GLOBAL / UNIVERSAL / COMPANY_SPECIFIC (behavior is implicit in `type`). * * - `'GLOBAL'` — global baseline + per-company overrides; company selector shows "Global" + companies * - `'COMPANY_SPECIFIC'` — independent config per company; no "Global" option * - `'NONE'` — no company context; company selector hidden */ companyBehavior?: 'GLOBAL' | 'COMPANY_SPECIFIC' | 'NONE'; /** * Structured form layout: fields grouped into titled sections. * When present, `BasicDataTab` renders section headers. * Use `getAllFields(definition)` to get the flat list for list/filter/validation. */ sections?: MasterPatternViewFormSection[]; /** * Flat field list — used when sections are not needed (no section titles). * Backward compatible: all existing definitions continue to work unchanged. */ fields?: MasterPatternViewFieldDefinition[]; /** Tab configuration for multi-tab forms */ tabs?: MasterPatternViewTabConfig[]; /** Default sort field and direction */ defaultSort?: { field: string; direction: 'asc' | 'desc'; }; } //# sourceMappingURL=definition.types.d.ts.map