@asteby/metacore-runtime-react 47.1.0 → 48.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +24 -4
  4. package/dist/business/catalog-product-search.d.ts +25 -0
  5. package/dist/business/catalog-product-search.d.ts.map +1 -0
  6. package/dist/business/catalog-product-search.js +57 -0
  7. package/dist/business/catalog.d.ts.map +1 -1
  8. package/dist/business/catalog.js +2 -0
  9. package/dist/business/document-editor-model.d.ts +107 -0
  10. package/dist/business/document-editor-model.d.ts.map +1 -0
  11. package/dist/business/document-editor-model.js +279 -0
  12. package/dist/business/document-editor-panels.d.ts +64 -0
  13. package/dist/business/document-editor-panels.d.ts.map +1 -0
  14. package/dist/business/document-editor-panels.js +58 -0
  15. package/dist/business/document-editor.d.ts +27 -0
  16. package/dist/business/document-editor.d.ts.map +1 -0
  17. package/dist/business/document-editor.js +445 -0
  18. package/dist/business/document-lines.d.ts.map +1 -1
  19. package/dist/business/document-lines.js +5 -1
  20. package/dist/business/index.d.ts +5 -0
  21. package/dist/business/index.d.ts.map +1 -1
  22. package/dist/business/index.js +5 -0
  23. package/dist/business/line-items.d.ts +7 -0
  24. package/dist/business/line-items.d.ts.map +1 -1
  25. package/dist/business/line-items.js +6 -1
  26. package/dist/business/payment-allocator.d.ts +19 -0
  27. package/dist/business/payment-allocator.d.ts.map +1 -0
  28. package/dist/business/payment-allocator.js +31 -0
  29. package/dist/business/product-search.d.ts +4 -0
  30. package/dist/business/product-search.d.ts.map +1 -1
  31. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  32. package/dist/dialogs/dynamic-record.js +14 -7
  33. package/dist/document-form-dialog.d.ts +31 -2
  34. package/dist/document-form-dialog.d.ts.map +1 -1
  35. package/dist/document-form-dialog.js +43 -11
  36. package/dist/dynamic-columns-helpers.d.ts +9 -0
  37. package/dist/dynamic-columns-helpers.d.ts.map +1 -1
  38. package/dist/dynamic-columns-helpers.js +19 -0
  39. package/dist/dynamic-columns.d.ts +8 -2
  40. package/dist/dynamic-columns.d.ts.map +1 -1
  41. package/dist/dynamic-columns.js +21 -5
  42. package/dist/dynamic-crud-page.d.ts +6 -0
  43. package/dist/dynamic-crud-page.d.ts.map +1 -1
  44. package/dist/dynamic-crud-page.js +14 -5
  45. package/dist/dynamic-form-schema.d.ts.map +1 -1
  46. package/dist/dynamic-form-schema.js +15 -3
  47. package/dist/dynamic-form.d.ts +16 -1
  48. package/dist/dynamic-form.d.ts.map +1 -1
  49. package/dist/dynamic-form.js +78 -20
  50. package/dist/dynamic-kanban.d.ts +9 -3
  51. package/dist/dynamic-kanban.d.ts.map +1 -1
  52. package/dist/dynamic-kanban.js +31 -12
  53. package/dist/dynamic-relation-helpers.d.ts.map +1 -1
  54. package/dist/dynamic-relation-helpers.js +8 -0
  55. package/dist/dynamic-relation.js +1 -1
  56. package/dist/index.d.ts +5 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +13 -2
  59. package/dist/installed-addons-context.d.ts +15 -0
  60. package/dist/installed-addons-context.d.ts.map +1 -0
  61. package/dist/installed-addons-context.js +39 -0
  62. package/dist/org-runtime-context.d.ts +8 -0
  63. package/dist/org-runtime-context.d.ts.map +1 -1
  64. package/dist/org-runtime-context.js +8 -0
  65. package/dist/org-runtime-provider.d.ts +3 -1
  66. package/dist/org-runtime-provider.d.ts.map +1 -1
  67. package/dist/org-runtime-provider.js +3 -3
  68. package/dist/primitives/aging.d.ts +34 -0
  69. package/dist/primitives/aging.d.ts.map +1 -0
  70. package/dist/primitives/aging.js +48 -0
  71. package/dist/primitives/allocation.d.ts +42 -0
  72. package/dist/primitives/allocation.d.ts.map +1 -0
  73. package/dist/primitives/allocation.js +59 -0
  74. package/dist/primitives/approvals.d.ts +71 -0
  75. package/dist/primitives/approvals.d.ts.map +1 -0
  76. package/dist/primitives/approvals.js +48 -0
  77. package/dist/primitives/contributions.d.ts +82 -0
  78. package/dist/primitives/contributions.d.ts.map +1 -0
  79. package/dist/primitives/contributions.js +126 -0
  80. package/dist/primitives/document-kinds.d.ts +25 -0
  81. package/dist/primitives/document-kinds.d.ts.map +1 -0
  82. package/dist/primitives/document-kinds.js +26 -0
  83. package/dist/primitives/index.d.ts +8 -0
  84. package/dist/primitives/index.d.ts.map +1 -0
  85. package/dist/primitives/index.js +23 -0
  86. package/dist/primitives/panels.d.ts +68 -0
  87. package/dist/primitives/panels.d.ts.map +1 -0
  88. package/dist/primitives/panels.js +3 -0
  89. package/dist/primitives/use-contributions.d.ts +15 -0
  90. package/dist/primitives/use-contributions.d.ts.map +1 -0
  91. package/dist/primitives/use-contributions.js +30 -0
  92. package/dist/record-prefill-bar.d.ts +11 -0
  93. package/dist/record-prefill-bar.d.ts.map +1 -0
  94. package/dist/record-prefill-bar.js +33 -0
  95. package/dist/types.d.ts +102 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +3 -3
  98. package/src/__tests__/catalog-product-search.test.ts +61 -0
  99. package/src/__tests__/document-editor-model.test.ts +167 -0
  100. package/src/__tests__/document-editor.test.tsx +261 -0
  101. package/src/__tests__/document-form-dialog.test.tsx +118 -3
  102. package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
  103. package/src/__tests__/dynamic-kanban.test.tsx +24 -3
  104. package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
  105. package/src/__tests__/installed-addons-context.test.tsx +50 -0
  106. package/src/__tests__/line-item-totals.test.ts +28 -0
  107. package/src/__tests__/primitives-federated.test.tsx +156 -0
  108. package/src/__tests__/primitives.test.ts +224 -0
  109. package/src/__tests__/record-prefill-bar.test.tsx +50 -0
  110. package/src/action-modal-dispatcher.tsx +47 -4
  111. package/src/business/catalog-product-search.ts +87 -0
  112. package/src/business/catalog.ts +2 -0
  113. package/src/business/document-editor-model.ts +325 -0
  114. package/src/business/document-editor-panels.tsx +164 -0
  115. package/src/business/document-editor.tsx +727 -0
  116. package/src/business/document-lines.ts +5 -1
  117. package/src/business/index.ts +5 -0
  118. package/src/business/line-items.ts +15 -1
  119. package/src/business/payment-allocator.tsx +110 -0
  120. package/src/business/product-search.ts +4 -0
  121. package/src/dialogs/dynamic-record.tsx +29 -6
  122. package/src/document-form-dialog.tsx +92 -12
  123. package/src/dynamic-columns-helpers.ts +22 -0
  124. package/src/dynamic-columns.tsx +23 -6
  125. package/src/dynamic-crud-page.tsx +22 -3
  126. package/src/dynamic-form-schema.ts +15 -2
  127. package/src/dynamic-form.tsx +117 -39
  128. package/src/dynamic-kanban.tsx +34 -7
  129. package/src/dynamic-relation-helpers.ts +9 -0
  130. package/src/dynamic-relation.tsx +7 -2
  131. package/src/index.ts +21 -1
  132. package/src/installed-addons-context.tsx +58 -0
  133. package/src/org-runtime-context.ts +10 -0
  134. package/src/org-runtime-provider.tsx +7 -2
  135. package/src/primitives/aging.ts +76 -0
  136. package/src/primitives/allocation.ts +104 -0
  137. package/src/primitives/approvals.ts +103 -0
  138. package/src/primitives/contributions.ts +208 -0
  139. package/src/primitives/document-kinds.ts +62 -0
  140. package/src/primitives/index.ts +23 -0
  141. package/src/primitives/panels.ts +59 -0
  142. package/src/primitives/use-contributions.ts +42 -0
  143. package/src/record-prefill-bar.tsx +62 -0
  144. package/src/row-action-stage-gate.test.ts +33 -0
  145. package/src/types.ts +101 -0
@@ -125,7 +125,17 @@ export function RowActionMenuItem({ action, label, onSelect, }) {
125
125
  * …) and fall back to `state` (purchases, inventory transfers, …). Empty string
126
126
  * is treated as missing so a blank `status` does not hide a populated `state`.
127
127
  */
128
- const rowLifecycleState = (row) => {
128
+ const rowLifecycleState = (row, stageField) => {
129
+ // A model with a stage machine gates its actions on the stage column, the
130
+ // same field the kernel's checkRequiresState reads (dynamic/action.go).
131
+ // Without this, WorkOrder actions declared with stage keys
132
+ // (requiresState: ['reception', …]) were compared to `status` ('open') and
133
+ // hidden in every row, so an OT could never be advanced from the UI.
134
+ if (stageField) {
135
+ const stage = row?.[stageField];
136
+ if (stage !== undefined && stage !== null && stage !== '')
137
+ return stage;
138
+ }
129
139
  const status = row?.status;
130
140
  if (status !== undefined && status !== null && status !== '')
131
141
  return status;
@@ -147,11 +157,11 @@ const rowLifecycleState = (row) => {
147
157
  * - action without requiresState (or empty array) → always shown.
148
158
  * - row with neither `status` nor `state` → all actions shown.
149
159
  */
150
- export const isActionAllowedForRowState = (action, row) => {
160
+ export const isActionAllowedForRowState = (action, row, stageField) => {
151
161
  const requires = action?.requiresState ?? action?.requires_state;
152
162
  if (!Array.isArray(requires) || requires.length === 0)
153
163
  return true;
154
- const status = rowLifecycleState(row);
164
+ const status = rowLifecycleState(row, stageField);
155
165
  if (status === undefined)
156
166
  return true;
157
167
  return requires.map(String).includes(String(status));
@@ -225,7 +235,13 @@ export const isActionConditionMet = (action, row) => {
225
235
  * (`requiresState`) AND the declarative `condition` must pass. Shared by the
226
236
  * table's action column and the kanban card menu so they hide/show identically.
227
237
  */
228
- export const isRowActionVisible = (action, row) => isActionAllowedForRowState(action, row) && isActionConditionMet(action, row);
238
+ export const isRowActionVisible = (action, row, stageField) => isActionAllowedForRowState(action, row, stageField) && isActionConditionMet(action, row);
239
+ /**
240
+ * The column a model's `requiresState` gates read: the served `stage_field`
241
+ * (or, for hosts that predate it, the `group_by` of a model with stages).
242
+ * Undefined → the historical `status` / `state` lookup.
243
+ */
244
+ export const lifecycleStageField = (metadata) => metadata?.stage_field ?? metadata?.stageField ?? (metadata?.stages?.length ? metadata?.group_by : undefined);
229
245
  const lowerFirst = (value) => {
230
246
  if (!value)
231
247
  return value;
@@ -1034,7 +1050,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
1034
1050
  maxSize: 80,
1035
1051
  meta: {},
1036
1052
  cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: resolvedActions
1037
- .filter((action) => isRowActionVisible(action, row.original))
1053
+ .filter((action) => isRowActionVisible(action, row.original, lifecycleStageField(metadata)))
1038
1054
  .map((action) => (_jsx(RowActionMenuItem, { action: action, label: translateMetadataLabel(action.label, t), onSelect: () => onAction && onAction(action.key, row.original) }, action.key))) })] }) })),
1039
1055
  });
1040
1056
  }
@@ -45,6 +45,12 @@ export interface DynamicCRUDPageProps {
45
45
  * with neither, the generic record dialog is used as before.
46
46
  */
47
47
  documentForms?: DocumentFormsManifest;
48
+ /**
49
+ * Filtro fijo de la vista (el `filter` del nav). Si va sobre el `type_field`
50
+ * del alta guiada, «Crear» abre ese tipo directo o se oculta cuando no hay
51
+ * formulario para él.
52
+ */
53
+ filter?: Record<string, string>;
48
54
  }
49
55
  /**
50
56
  * Page-level CRUD shell wired around <DynamicTable>. Hosts mount a single
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAad,OAAO,KAAK,EAAE,qBAAqB,EAAiB,MAAM,SAAS,CAAA;AAEnE,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,qBAAqB,CAAA;CACxC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,qBA6N1D"}
1
+ {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAad,OAAO,KAAK,EAAE,qBAAqB,EAAiB,MAAM,SAAS,CAAA;AAEnE,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,qBA0O1D"}
@@ -29,7 +29,7 @@ import { useApi } from './api-context';
29
29
  import { useMetadataCache } from './metadata-cache';
30
30
  import { DynamicTable } from './dynamic-table';
31
31
  import { DynamicRecordDialog } from './dialogs/dynamic-record';
32
- import { DocumentFormDialog, resolveDocumentForms } from './document-form-dialog';
32
+ import { DocumentFormDialog, resolveDocumentForms, scopeDocumentFormsToFilter } from './document-form-dialog';
33
33
  import { ExportDialog } from './dialogs/export';
34
34
  import { ImportDialog } from './dialogs/import';
35
35
  import { getModelExtension } from './model-extension-registry';
@@ -48,7 +48,7 @@ const defaultStrings = {
48
48
  * metadata fetch, dialogs and toolbar.
49
49
  */
50
50
  export function DynamicCRUDPage(props) {
51
- const { model, endpoint, title: titleOverride, newLabel, i18n, hideCreate, hideExport, hideImport, hideRefresh, headerExtras, toolbarExtras, classes, onChange, documentForms, } = props;
51
+ const { model, endpoint, title: titleOverride, newLabel, i18n, hideCreate, hideExport, hideImport, hideRefresh, headerExtras, toolbarExtras, classes, onChange, documentForms, filter, } = props;
52
52
  const strings = { ...defaultStrings, ...(i18n ?? {}) };
53
53
  const dataEndpoint = endpoint ?? `/dynamic/${model}`;
54
54
  const ext = getModelExtension(model);
@@ -114,11 +114,20 @@ export function DynamicCRUDPage(props) {
114
114
  // (useCan defaults to always-true), in which case create/export/import
115
115
  // require `lowercase(model).create|export|import`.
116
116
  const can = useCan();
117
- const showCreate = primary.showGenericCreate && can(modelCapability(model, 'create'));
117
+ const scoped = scopeDocumentFormsToFilter(resolveDocumentForms(metadata, documentForms), filter);
118
+ const guidedForms = scoped.forms;
119
+ const showCreate = primary.showGenericCreate && !scoped.hideCreate && can(modelCapability(model, 'create'));
118
120
  const showImport = enableCRUD && !effectiveHideImport && can(modelCapability(model, 'import'));
119
121
  const showExport = !effectiveHideExport && can(modelCapability(model, 'export'));
120
122
  const showRefresh = !effectiveHideRefresh;
121
- const guidedForms = resolveDocumentForms(metadata, documentForms);
123
+ // El mismo filtro acota la tabla (forma `eq:<valor>` que espera DynamicTable).
124
+ const filterKey = filter ? JSON.stringify(filter) : '';
125
+ const tableFilters = useMemo(() => {
126
+ if (!filter || Object.keys(filter).length === 0)
127
+ return undefined;
128
+ return Object.fromEntries(Object.entries(filter).map(([k, v]) => [k, String(v).includes(':') ? String(v) : `eq:${v}`]));
129
+ // eslint-disable-next-line react-hooks/exhaustive-deps
130
+ }, [filterKey]);
122
131
  const handleRefresh = useCallback(() => {
123
132
  setRefreshKey((k) => k + 1);
124
133
  onChange?.();
@@ -129,5 +138,5 @@ export function DynamicCRUDPage(props) {
129
138
  const titleCls = classes?.title ?? 'text-2xl font-bold tracking-tight';
130
139
  const toolbarCls = classes?.toolbar ?? 'flex items-center gap-2';
131
140
  const tableWrapperCls = classes?.tableWrapper ?? 'flex-1 min-h-0';
132
- return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, _jsx(ModelActionToolbar, { model: model, endpoint: dataEndpoint, actions: metadata?.actions, primaryActionKey: ext?.primaryActionKey, onChange: handleRefresh }), showCreate && (_jsxs("button", { type: 'button', "data-primary": 'true', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey }, model) })] }), showCreate && guidedForms && (_jsx(DocumentFormDialog, { open: openCreate, onOpenChange: setOpenCreate, model: model, endpoint: dataEndpoint, forms: guidedForms, onSaved: handleRefresh })), showCreate && !guidedForms && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
141
+ return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, _jsx(ModelActionToolbar, { model: model, endpoint: dataEndpoint, actions: metadata?.actions, primaryActionKey: ext?.primaryActionKey, onChange: handleRefresh }), showCreate && (_jsxs("button", { type: 'button', "data-primary": 'true', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey, defaultFilters: tableFilters }, model) })] }), showCreate && guidedForms && (_jsx(DocumentFormDialog, { open: openCreate, onOpenChange: setOpenCreate, model: model, endpoint: dataEndpoint, forms: guidedForms, onSaved: handleRefresh })), showCreate && !guidedForms && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
133
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAkBzF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAwBD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACpC,UAAU,EAAE,cAAc,EAAE,EAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAuBrB;AAcD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,cAAc,EAAE,EAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxB,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC3D,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqCrB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAiC3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,cAAc,EACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACvC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GACvC,MAAM,CAQR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC3B,OAAO,EAAE,SAAS,EAAE,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,SAAS,CAAC,EAAE,MAAM,GACnB,SAAS,EAAE,CAqBb;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE;IAAE,YAAY,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GACpF,WAAW,GAAG,SAAS,CAIzB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,wBAAwB,CAAA;AAE1D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,GACnD,OAAO,CAiBT;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAqB9D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAChC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAC/C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAC3C,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,GAC9C,OAAO,CAQT;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,kBAAkB,GAAG,SAAS,CAGtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACnB,CAQA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB,CAaA"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
@@ -97,14 +97,26 @@ export function toNumber(v) {
97
97
  * drift (0.1 + 0.2 noise) that would make a genuinely balanced entry look off.
98
98
  */
99
99
  export function computeLineItemTotals(field, rows) {
100
- const cols = getItemFields(field).filter((c) => c.total);
100
+ const itemFields = getItemFields(field);
101
+ const cols = itemFields.filter((c) => c.total);
102
+ // A `total` UNIT PRICE column on a grid with a quantity and no line amount
103
+ // column sums the line amounts (qty × price − discount): adding unit prices
104
+ // is meaningless once a quantity exists (2 × 750 footed 750, not 1500).
105
+ const qtyKey = firstKey(itemFields, LINE_QTY_KEYS);
106
+ const lineValued = !!qtyKey && !firstKey(itemFields, LINE_AMOUNT_KEYS);
107
+ const discountKey = firstKey(itemFields, LINE_DISCOUNT_KEYS);
101
108
  const totals = {};
102
109
  for (const c of cols)
103
110
  totals[c.key] = 0;
104
111
  if (Array.isArray(rows)) {
105
112
  for (const row of rows) {
106
- for (const c of cols)
107
- totals[c.key] += toNumber(row?.[c.key]);
113
+ for (const c of cols) {
114
+ const v = toNumber(row?.[c.key]);
115
+ totals[c.key] +=
116
+ lineValued && LINE_PRICE_KEYS.has(c.key)
117
+ ? toNumber(row?.[qtyKey]) * v - (discountKey ? toNumber(row?.[discountKey]) : 0)
118
+ : v;
119
+ }
108
120
  }
109
121
  }
110
122
  for (const k of Object.keys(totals))
@@ -25,6 +25,21 @@ export interface DynamicFormProps {
25
25
  * legacy flat list (unchanged).
26
26
  */
27
27
  formLayout?: FormLayout;
28
+ /**
29
+ * Para formularios dentro de un modal con alto acotado: el cuerpo de campos
30
+ * hace scroll propio y la barra de botones (Guardar/Cancelar) queda fija
31
+ * abajo, así nunca queda fuera de pantalla con muchos campos. El contenedor
32
+ * debe ser flex-col con alto máximo (p.ej. `max-h-[90dvh]`).
33
+ */
34
+ scrollableBody?: boolean;
28
35
  }
29
- export declare function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel, cancelLabel, disabled, formLayout, }: DynamicFormProps): import("react").JSX.Element;
36
+ export declare function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel, cancelLabel, disabled, formLayout, scrollableBody, }: DynamicFormProps): import("react").JSX.Element;
37
+ /**
38
+ * Radix Select puede emitir `onValueChange('')` sin interacción del usuario (su
39
+ * <select> nativo oculto colapsa a '' si recibe el valor antes que sus
40
+ * <option>s, p.ej. dentro de un <form> en el primer render). Ese '' pisaría el
41
+ * valor real del registro y viajaría en el PATCH. Se ignora cuando ya hay un
42
+ * valor y '' no es una opción válida — el usuario nunca puede elegir '' así.
43
+ */
44
+ export declare function guardEmptySelect(current: unknown, onChange: (v: any) => void, optionValues: readonly unknown[]): (v: string) => void;
30
45
  //# sourceMappingURL=dynamic-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAerE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAOhB,MAAM,uBAAuB,CAAA;AAa9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEzF,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,EAChB,UAAU,GACb,EAAE,gBAAgB,+BAwMlB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAerE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAOhB,MAAM,uBAAuB,CAAA;AAa9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEzF,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAsBD,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,EAChB,UAAU,EACV,cAAsB,GACzB,EAAE,gBAAgB,+BAiOlB;AA4PD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,EAC1B,YAAY,EAAE,SAAS,OAAO,EAAE,GACjC,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAMrB"}
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Minimal standalone DynamicForm. Factored from the dynamic-record-dialog
3
3
  // pattern + ActionFieldDef renderer so callers can reuse the form layout
4
4
  // outside the full record-edit modal.
5
- import { useEffect, useMemo, useState } from 'react';
5
+ import { useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { groupFieldsBySection } from './form-layout';
7
7
  import { FieldSection, WizardProgress } from './form-layout-ui';
8
8
  import { AssistInterview } from './assist-interview';
@@ -27,8 +27,24 @@ export { DynamicDateField } from './dynamic-date-field';
27
27
  export { UploadField } from './upload-field';
28
28
  export { IconPickerField } from './icon-picker-field';
29
29
  export { ColorPickerField, DEFAULT_ROLE_COLOR, normalizeHex } from './color-picker-field';
30
- export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, formLayout, }) {
31
- const [values, setValues] = useState({});
30
+ // Valores iniciales del formulario: el registro (edición) gana sobre el default
31
+ // del campo. Se calcula de forma síncrona en el primer render para que ningún
32
+ // widget (p.ej. un Select de Radix) monte con '' y emita un cambio espurio que
33
+ // pise el valor real del registro.
34
+ function seedValues(fields, initialValues) {
35
+ const defaults = {};
36
+ for (const f of fields) {
37
+ if (isLineItemsField(f)) {
38
+ defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? [];
39
+ continue;
40
+ }
41
+ defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? (f.type === 'boolean' ? false : '');
42
+ }
43
+ return defaults;
44
+ }
45
+ const isReadonlyField = (f) => !!f.readonly ||
46
+ !!f.readOnly;
47
+ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, formLayout, scrollableBody = false, }) {
32
48
  const [errors, setErrors] = useState({});
33
49
  const [submitting, setSubmitting] = useState(false);
34
50
  // Readonly fields are system-owned (connection state, sync-written values):
@@ -36,7 +52,11 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
36
52
  // instead of rendered disabled, matching the metadata contract
37
53
  // (ColumnDef.Readonly). They are also excluded from defaults/submit so a
38
54
  // create never posts e.g. `status: ""` over the column default.
39
- const editableFields = useMemo(() => fields.filter((f) => !f.readonly), [fields]);
55
+ const editableFields = useMemo(() => fields.filter((f) => !isReadonlyField(f)), [fields]);
56
+ const [values, setValues] = useState(() => seedValues(editableFields, initialValues));
57
+ // Campos que el usuario (o un reset dependiente) cambió desde la siembra.
58
+ // En edición, un select que sigue vacío y nadie tocó no viaja en el PATCH.
59
+ const touched = useRef(new Set());
40
60
  // Conditional visibility: a field carrying `visible_when` is rendered — and
41
61
  // validated — only while the referenced sibling field's current value
42
62
  // matches the predicate. Driving BOTH the schema and the render off the same
@@ -69,19 +89,22 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
69
89
  useEffect(() => {
70
90
  setStepIndex((i) => Math.min(i, Math.max(groups.length - 1, 0)));
71
91
  }, [groups.length]);
92
+ // Re-siembra cuando cambia el registro o los campos DESPUÉS del montaje (el
93
+ // primer render ya quedó sembrado por el inicializador de useState).
94
+ const seededFrom = useRef({ editableFields, initialValues });
72
95
  useEffect(() => {
73
- const defaults = {};
74
- for (const f of editableFields) {
75
- if (isLineItemsField(f)) {
76
- defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? [];
77
- continue;
78
- }
79
- defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? (f.type === 'boolean' ? false : '');
80
- }
81
- setValues(defaults);
96
+ const prev = seededFrom.current;
97
+ if (prev.editableFields === editableFields && prev.initialValues === initialValues)
98
+ return;
99
+ seededFrom.current = { editableFields, initialValues };
100
+ touched.current = new Set();
101
+ setValues(seedValues(editableFields, initialValues));
82
102
  setErrors({});
83
103
  }, [editableFields, initialValues]);
84
- const update = (k, v) => setValues((prev) => ({ ...prev, [k]: v }));
104
+ const update = (k, v) => {
105
+ touched.current.add(k);
106
+ setValues((prev) => ({ ...prev, [k]: v }));
107
+ };
85
108
  const handleSubmit = async (e) => {
86
109
  e.preventDefault();
87
110
  if (balanceBlocked)
@@ -98,9 +121,23 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
98
121
  return;
99
122
  }
100
123
  setErrors({});
124
+ const data = { ...result.data };
125
+ // Edición: un select vacío que el usuario no tocó no se manda — el update
126
+ // es parcial, así que omitirlo conserva lo que tenga el registro en vez de
127
+ // pisarlo con ''.
128
+ if (initialValues) {
129
+ for (const f of visibleFields) {
130
+ if (resolveWidget(f) !== 'select')
131
+ continue;
132
+ if (touched.current.has(f.key))
133
+ continue;
134
+ if (data[f.key] === '' || data[f.key] == null)
135
+ delete data[f.key];
136
+ }
137
+ }
101
138
  setSubmitting(true);
102
139
  try {
103
- await onSubmit(result.data);
140
+ await onSubmit(data);
104
141
  }
105
142
  finally {
106
143
  setSubmitting(false);
@@ -110,6 +147,10 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
110
147
  // header fields flow through it; line-items grids (and textareas) span full
111
148
  // width so the row table / memo gets room. Shared by every layout mode.
112
149
  const renderGrid = (groupFields) => (_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: groupFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }));
150
+ // scrollableBody: el cuerpo hace scroll y el footer de botones queda fijo.
151
+ const formClass = scrollableBody ? 'flex min-h-0 flex-1 flex-col gap-4' : 'grid gap-4';
152
+ const bodyClass = scrollableBody ? '-mx-1 grid min-h-0 flex-1 gap-4 overflow-y-auto px-1' : 'grid gap-4';
153
+ const footerExtra = scrollableBody ? ' shrink-0 border-t' : '';
113
154
  // ── Steps (wizard) mode ────────────────────────────────────────────────
114
155
  // One step per section; Anterior/Siguiente navigate, submit only on the
115
156
  // last step. Advancing validates just the current step's fields so a later
@@ -134,7 +175,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
134
175
  setStepIndex((i) => Math.min(i + 1, groups.length - 1));
135
176
  };
136
177
  const goBack = () => setStepIndex((i) => Math.max(i - 1, 0));
137
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx(WizardProgress, { groups: groups, stepIndex: stepIndex, onStepClick: i => setStepIndex(i) }), step.assist ? (_jsx(AssistInterview, { assist: step.assist, values: values, eyebrow: step.title, autoStart: step.assist.trigger !== 'button', onApply: fields => setValues(prev => ({ ...prev, ...fields })) })) : (renderGrid(step.fields)), _jsxs("div", { className: "flex justify-between gap-2 pt-2", children: [stepIndex > 0 ? (_jsx(Button, { type: "button", variant: "outline", onClick: goBack, disabled: submitting || disabled, children: "Anterior" })) : onCancel ? (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })) : (_jsx("span", {})), isLast ? (_jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })) : (_jsx(Button, { type: "button", onClick: goNext, disabled: submitting || disabled, children: "Siguiente" }))] })] }));
178
+ return (_jsxs("form", { onSubmit: handleSubmit, className: formClass, children: [_jsxs("div", { className: bodyClass, children: [_jsx(WizardProgress, { groups: groups, stepIndex: stepIndex, onStepClick: i => setStepIndex(i) }), step.assist ? (_jsx(AssistInterview, { assist: step.assist, values: values, eyebrow: step.title, autoStart: step.assist.trigger !== 'button', onApply: fields => setValues(prev => ({ ...prev, ...fields })) })) : (renderGrid(step.fields))] }), _jsxs("div", { className: 'flex justify-between gap-2 pt-2' + footerExtra, children: [stepIndex > 0 ? (_jsx(Button, { type: "button", variant: "outline", onClick: goBack, disabled: submitting || disabled, children: "Anterior" })) : onCancel ? (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })) : (_jsx("span", {})), isLast ? (_jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })) : (_jsx(Button, { type: "button", onClick: goNext, disabled: submitting || disabled, children: "Siguiente" }))] })] }));
138
179
  }
139
180
  // ── Sections / flat mode ───────────────────────────────────────────────
140
181
  // Layout: scalar header fields flow through a responsive 2-column grid;
@@ -142,7 +183,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
142
183
  // memo gets room. Mirrors the pro look of the federated journal modal but
143
184
  // stays fully declarative — driven only by field shape. With no layout this
144
185
  // is a single default group rendered without section chrome (unchanged).
145
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [groups.map((group) => (_jsx(FieldSection, { group: group, children: renderGrid(group.fields) }, group.key))), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
186
+ return (_jsxs("form", { onSubmit: handleSubmit, className: formClass, children: [_jsx("div", { className: bodyClass, children: groups.map((group) => (_jsx(FieldSection, { group: group, children: renderGrid(group.fields) }, group.key))) }), _jsxs("div", { className: 'flex justify-end gap-2 pt-2' + footerExtra, children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
146
187
  }
147
188
  // One form field row: label + renderer + inline error. Encapsulated as its own
148
189
  // component so a STATIC enum select whose options are all gated out by a sibling
@@ -243,8 +284,10 @@ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions
243
284
  return _jsx(Textarea, { id: field.key, "data-widget": "richtext", value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
244
285
  case 'color':
245
286
  return (_jsx(ColorPickerField, { "aria-label": field.label || field.key, value: typeof value === 'string' ? value : '', onChange: onChange }));
246
- case 'select':
247
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: (effectiveOptions ?? field.options)?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
287
+ case 'select': {
288
+ const opts = effectiveOptions ?? field.options;
289
+ return (_jsxs(Select, { value: value || '', onValueChange: guardEmptySelect(value, onChange, (opts ?? []).map((o) => o.value)), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: opts?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
290
+ }
248
291
  case 'switch':
249
292
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
250
293
  case 'number':
@@ -291,6 +334,21 @@ function ScannableInput({ field, value, onChange, type, }) {
291
334
  // (default) porque suma varios productos seguidos.
292
335
  continuous: false, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() })] }));
293
336
  }
337
+ /**
338
+ * Radix Select puede emitir `onValueChange('')` sin interacción del usuario (su
339
+ * <select> nativo oculto colapsa a '' si recibe el valor antes que sus
340
+ * <option>s, p.ej. dentro de un <form> en el primer render). Ese '' pisaría el
341
+ * valor real del registro y viajaría en el PATCH. Se ignora cuando ya hay un
342
+ * valor y '' no es una opción válida — el usuario nunca puede elegir '' así.
343
+ */
344
+ export function guardEmptySelect(current, onChange, optionValues) {
345
+ return (v) => {
346
+ const hasValue = current !== undefined && current !== null && current !== '';
347
+ if (v === '' && hasValue && !optionValues.some((o) => String(o) === ''))
348
+ return;
349
+ onChange(v);
350
+ };
351
+ }
294
352
  function RefSelect({ field, value, onChange }) {
295
353
  const { options, loading } = useOptionsResolver({
296
354
  modelKey: '', // unused — `ref` drives the URL
@@ -299,5 +357,5 @@ function RefSelect({ field, value, onChange }) {
299
357
  optionFilter: getOptionFilter(field),
300
358
  keepValue: value,
301
359
  });
302
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: loading, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
360
+ return (_jsxs(Select, { value: value || '', onValueChange: guardEmptySelect(value, onChange, options.map((o) => String(o.id))), disabled: loading, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
303
361
  }
@@ -91,9 +91,15 @@ export declare function isCardOverdue(card: any, col: ColumnDefinition, now?: nu
91
91
  * The stage machine (stages, transitions, smart lanes) belongs to the model's
92
92
  * stage column (`stage_field`; older hosts that do not serve it: the served
93
93
  * group_by): grouping by it keeps the machine, grouping by anything else drops
94
- * it and the lanes come from the new column. Returns the SAME object when there
95
- * is nothing to change (no override, the served group_by already, or a column the
96
- * model does not have). Pure — exported for unit tests.
94
+ * it and the lanes come from the new column.
95
+ *
96
+ * The served group_by is the LAST kanban nav entry's, which need not be the
97
+ * stage column: a board opened without `?group_by=` (the list's view toggle)
98
+ * groups by the stage column, and a board grouped by the served column keeps
99
+ * the machine only when that column IS the stage column. Otherwise the stage
100
+ * lanes were painted over another column (by bay: every card under «Sin etapa»
101
+ * and a drop wrote `service_bay_id = 'diagnosis'`, 422). Returns the SAME
102
+ * object when there is nothing to change. Pure — exported for unit tests.
97
103
  */
98
104
  export declare function withGroupBy(metadata: TableMetadata | null, groupBy: string | undefined): TableMetadata | null;
99
105
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -37,7 +37,7 @@ import { useRecordMutationTick } from './use-record-mutation-tick';
37
37
  import { useOptionsResolver } from './use-options-resolver';
38
38
  import { ActivityValueRenderer } from './activity-value-renderer';
39
39
  import { isColumnVisibleInTable } from './column-visibility';
40
- import { aggregateOf, formatAggregateTotal, isRowActionVisible, relationKeyFor, RowActionMenuItem, } from './dynamic-columns';
40
+ import { aggregateOf, formatAggregateTotal, isRowActionVisible, lifecycleStageField, relationKeyFor, RowActionMenuItem, } from './dynamic-columns';
41
41
  import { useCan, usePermissionsActive, resolveRowActions } from './permissions-context';
42
42
  import { useDynamicRowActions } from './dynamic-row-actions';
43
43
  import { useStageLayout } from './stage-layout';
@@ -251,18 +251,37 @@ export function isCardOverdue(card, col, now = Date.now()) {
251
251
  * The stage machine (stages, transitions, smart lanes) belongs to the model's
252
252
  * stage column (`stage_field`; older hosts that do not serve it: the served
253
253
  * group_by): grouping by it keeps the machine, grouping by anything else drops
254
- * it and the lanes come from the new column. Returns the SAME object when there
255
- * is nothing to change (no override, the served group_by already, or a column the
256
- * model does not have). Pure — exported for unit tests.
254
+ * it and the lanes come from the new column.
255
+ *
256
+ * The served group_by is the LAST kanban nav entry's, which need not be the
257
+ * stage column: a board opened without `?group_by=` (the list's view toggle)
258
+ * groups by the stage column, and a board grouped by the served column keeps
259
+ * the machine only when that column IS the stage column. Otherwise the stage
260
+ * lanes were painted over another column (by bay: every card under «Sin etapa»
261
+ * and a drop wrote `service_bay_id = 'diagnosis'`, 422). Returns the SAME
262
+ * object when there is nothing to change. Pure — exported for unit tests.
257
263
  */
258
264
  export function withGroupBy(metadata, groupBy) {
259
- if (!metadata || !groupBy || groupBy === metadata.group_by)
265
+ if (!metadata)
260
266
  return metadata;
267
+ const hasMachine = (metadata.stages?.length ?? 0) > 0;
268
+ const stageColumn = metadata.stage_field ?? (hasMachine ? metadata.group_by : undefined);
269
+ if (!groupBy) {
270
+ if (!hasMachine || !stageColumn || stageColumn === metadata.group_by)
271
+ return metadata;
272
+ return { ...metadata, group_by: stageColumn };
273
+ }
274
+ if (groupBy === stageColumn) {
275
+ return groupBy === metadata.group_by ? metadata : { ...metadata, group_by: groupBy };
276
+ }
261
277
  if (!metadata.columns.some((c) => c.key === groupBy))
262
278
  return metadata;
263
- const stageColumn = metadata.stage_field ?? (metadata.stages?.length ? metadata.group_by : undefined);
264
- if (groupBy === stageColumn)
265
- return { ...metadata, group_by: groupBy };
279
+ if (groupBy === metadata.group_by &&
280
+ !hasMachine &&
281
+ !metadata.transitions &&
282
+ !metadata.smart_lanes) {
283
+ return metadata;
284
+ }
266
285
  return {
267
286
  ...metadata,
268
287
  group_by: groupBy,
@@ -1550,7 +1569,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
1550
1569
  extraFilters,
1551
1570
  configurable: isConfigurable(stage),
1552
1571
  onConfigure: () => openConfigStage(stage),
1553
- children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, pending: card.id != null && String(card.id) in moveOverrides }, String(card.id))))),
1572
+ children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, stageField: lifecycleStageField(metadata), locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, pending: card.id != null && String(card.id) in moveOverrides }, String(card.id))))),
1554
1573
  };
1555
1574
  };
1556
1575
  // Props for one smart (virtual) lane — a read-only, filter-defined column.
@@ -1564,7 +1583,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
1564
1583
  refreshTrigger,
1565
1584
  onEdit: openEditStage,
1566
1585
  onDelete: openDeleteStage,
1567
- renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })),
1586
+ renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, stageField: lifecycleStageField(metadata), locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })),
1568
1587
  });
1569
1588
  const showResetOrder = laneReorderEnabled && (stageLayout.hasCustomLayout || !!laneOrderOverride);
1570
1589
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", "data-testid": "kanban-filters", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t('kanban.searchPlaceholder', { defaultValue: 'Buscar...' }), className: "h-8 w-52 pl-8 text-sm" })] }), filterFields.length > 0 && (_jsxs(Sheet, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", children: [_jsx(ListFilter, { className: "h-3.5 w-3.5" }), t('kanban.filters', { defaultValue: 'Filtros' }), activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-0.5 h-4 min-w-4 justify-center rounded-full px-1 text-[10px] tabular-nums", children: activeFilterCount }))] }) }), _jsxs(SheetContent, { side: "right", className: "flex w-80 flex-col gap-0 p-0 sm:max-w-sm", children: [_jsx(SheetHeader, { className: "space-y-0 border-b px-4 py-3", children: _jsx(SheetTitle, { className: "text-sm", children: t('kanban.filters', { defaultValue: 'Filtros' }) }) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1 overflow-y-auto px-4 py-3", children: [activeFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "px-0.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: t('kanban.activeFilters', {
@@ -1882,13 +1901,13 @@ function CardBody({ card, titleCol, fieldCols, showAllFields, locale, timeZone,
1882
1901
  ? 'font-medium text-destructive'
1883
1902
  : 'text-muted-foreground'}`, "data-overdue": isCardOverdue(card, col) || undefined, children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 break-words", children: _jsx(ActivityValueRenderer, { value: cardCellValue(card, col), col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }));
1884
1903
  }
1885
- function KanbanCard({ card, titleCol, fieldCols, showAllFields, actions, locale, timeZone, currency, onClick, onAction, draggable = true, pending = false, }) {
1904
+ function KanbanCard({ card, titleCol, fieldCols, showAllFields, actions, stageField, locale, timeZone, currency, onClick, onAction, draggable = true, pending = false, }) {
1886
1905
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
1887
1906
  id: String(card.id),
1888
1907
  data: { type: 'card' },
1889
1908
  disabled: !draggable,
1890
1909
  });
1891
- const visibleActions = actions.filter((a) => isRowActionVisible(a, card));
1910
+ const visibleActions = actions.filter((a) => isRowActionVisible(a, card, stageField));
1892
1911
  const menu = visibleActions.length > 0 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0 -mr-1 -mt-1",
1893
1912
  // Don't start a drag / card click from the menu button.
1894
1913
  onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", onClick: (e) => e.stopPropagation(), children: visibleActions.map((a) => (_jsx(RowActionMenuItem, { action: a, label: a.label, onSelect: (e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"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"}
@@ -151,10 +151,18 @@ export function deriveRelationFormFields(metadata, foreignKey) {
151
151
  // than a raw uuid text input. Without this the column lost its `ref`
152
152
  // crossing the column→field boundary and degraded to plain text.
153
153
  ref: col.ref,
154
+ // Columnas de solo lectura (escritas por el servidor/sistema): el
155
+ // DynamicForm las oculta y no las manda en el submit. Sin copiarla,
156
+ // el renglón las exponía editables y el PATCH las pisaba.
157
+ ...(isReadonlyColumn(col) ? { readonly: true } : {}),
154
158
  });
155
159
  }
156
160
  return out;
157
161
  }
162
+ function isReadonlyColumn(col) {
163
+ const c = col;
164
+ return c.readonly === true || c.readOnly === true || c.read_only === true;
165
+ }
158
166
  function columnTypeToFieldType(col) {
159
167
  switch (col.type) {
160
168
  case 'number': return 'number';
@@ -315,7 +315,7 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
315
315
  }) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }), loadingMore && (_jsx("div", { className: "p-2", children: _jsx(Skeleton, { className: "h-8 w-full" }) })), _jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "h-px w-full" })] }) })), !loading && rows.length > 0 && total > rows.length && (_jsx("div", { className: "pt-2 text-end text-xs text-muted-foreground", children: labels.countLabel
316
316
  .replace('{{loaded}}', String(rows.length))
317
317
  .replace('{{total}}', String(total)) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
318
- setEditingRow(null); }, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting })] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
318
+ setEditingRow(null); }, children: _jsxs(DialogContent, { className: "flex max-h-[90dvh] flex-col overflow-hidden", children: [_jsx(DialogHeader, { className: "shrink-0", children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { scrollableBody: true, fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting }, editingRow ? `edit-${editingRow.id ?? ''}` : 'new')] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
319
319
  }
320
320
  function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, readonly, readOnly, strings, className, onChange, }) {
321
321
  // Read-only relation always wins over host-passed perms (AND).