@asteby/metacore-runtime-react 23.8.0 → 23.9.1

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 (38) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/custom-stages.d.ts +96 -1
  3. package/dist/custom-stages.d.ts.map +1 -1
  4. package/dist/custom-stages.js +259 -24
  5. package/dist/dynamic-form-schema.d.ts +18 -1
  6. package/dist/dynamic-form-schema.d.ts.map +1 -1
  7. package/dist/dynamic-form-schema.js +43 -0
  8. package/dist/dynamic-form.d.ts.map +1 -1
  9. package/dist/dynamic-form.js +30 -9
  10. package/dist/dynamic-kanban.d.ts +8 -0
  11. package/dist/dynamic-kanban.d.ts.map +1 -1
  12. package/dist/dynamic-kanban.js +188 -16
  13. package/dist/dynamic-line-items.d.ts.map +1 -1
  14. package/dist/dynamic-line-items.js +24 -3
  15. package/dist/dynamic-table.d.ts.map +1 -1
  16. package/dist/dynamic-table.js +5 -0
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/stage-overrides.d.ts +25 -0
  21. package/dist/stage-overrides.d.ts.map +1 -0
  22. package/dist/stage-overrides.js +64 -0
  23. package/dist/types.d.ts +69 -4
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +3 -3
  26. package/src/__tests__/dependent-enum-form.test.tsx +84 -0
  27. package/src/__tests__/dependent-enum-options.test.ts +67 -0
  28. package/src/__tests__/dynamic-kanban.test.tsx +75 -0
  29. package/src/__tests__/stage-overrides.test.tsx +293 -0
  30. package/src/custom-stages.tsx +716 -109
  31. package/src/dynamic-form-schema.ts +44 -1
  32. package/src/dynamic-form.tsx +76 -28
  33. package/src/dynamic-kanban.tsx +236 -25
  34. package/src/dynamic-line-items.tsx +26 -2
  35. package/src/dynamic-table.tsx +4 -0
  36. package/src/index.ts +13 -0
  37. package/src/stage-overrides.ts +95 -0
  38. package/src/types.ts +63 -1
@@ -265,6 +265,49 @@ export function resolveDependsValue(field, formValues, rowValues) {
265
265
  return '';
266
266
  return String(raw);
267
267
  }
268
+ /**
269
+ * Filters a STATIC enum's `options[]` by each option's `when` gate against the
270
+ * current form values. Pure — no React, no side effects.
271
+ *
272
+ * Rule per option:
273
+ * - No `when` → always included (retrocompat; existing enums untouched).
274
+ * - With `when`: the gating field is `when.field ?? dependsOn`. If neither is
275
+ * present the option is included (nothing to gate on). Otherwise the form's
276
+ * value for that field is compared AS STRING: included when (no `in`, or
277
+ * value ∈ `in`) AND (no `not_in`, or value ∉ `not_in`). Tolerates the
278
+ * snake_case `not_in` the kernel serves alongside camelCase `notIn`.
279
+ *
280
+ * `formValues` is the flat map of the surrounding form/row values the gating
281
+ * field is read from; `dependsOn` is the containing field's declared dependency
282
+ * used as the default gating field.
283
+ */
284
+ export function applyOptionWhen(options, formValues, dependsOn) {
285
+ if (!Array.isArray(options))
286
+ return [];
287
+ return options.filter((opt) => {
288
+ const when = opt?.when;
289
+ if (!when)
290
+ return true;
291
+ const gate = (typeof when.field === 'string' && when.field.trim() !== '')
292
+ ? when.field.trim()
293
+ : dependsOn;
294
+ if (!gate)
295
+ return true;
296
+ const raw = formValues ? formValues[gate] : undefined;
297
+ const current = raw == null ? '' : String(raw);
298
+ const inList = when.in;
299
+ const notIn = when.notIn ?? when.not_in;
300
+ if (Array.isArray(inList) && inList.length > 0) {
301
+ if (!inList.some((v) => String(v) === current))
302
+ return false;
303
+ }
304
+ if (Array.isArray(notIn) && notIn.length > 0) {
305
+ if (notIn.some((v) => String(v) === current))
306
+ return false;
307
+ }
308
+ return true;
309
+ });
310
+ }
268
311
  /**
269
312
  * Reads a field's enriched options-resolution config, tolerating the camelCase
270
313
  * `optionsConfig` (authored SDK shape) and the snake_case `options_config` the
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAGhB,MAAM,uBAAuB,CAAA;AAO9B,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,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,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;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BAmGlB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,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,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,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;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BAoFlB"}
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // outside the full record-edit modal.
5
5
  import { useEffect, useMemo, useState } from 'react';
6
6
  import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
7
- import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, } from './dynamic-form-schema';
7
+ import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, applyOptionWhen, getDependsOn, } from './dynamic-form-schema';
8
8
  import { useOptionsResolver } from './use-options-resolver';
9
9
  import { DynamicLineItems } from './dynamic-line-items';
10
10
  import { DynamicSelectField } from './dynamic-select-field';
@@ -73,12 +73,33 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
73
73
  // line-items grids (and textareas) span the full width so the row table /
74
74
  // memo gets room. Mirrors the pro look of the federated journal modal but
75
75
  // stays fully declarative — driven only by field shape.
76
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: fields.map((field) => {
77
- const fullWidth = isLineItemsField(field) ||
78
- resolveWidget(field) === 'textarea' ||
79
- resolveWidget(field) === 'richtext';
80
- return (_jsxs("div", { className: 'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : ''), children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), _jsx(FieldRenderer, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), initialValues: initialValues }), errors[field.key] && (_jsx("span", { className: "text-red-500 text-sm", role: "alert", children: errors[field.key] }))] }, field.key));
81
- }) }), _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 })] })] }));
76
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: fields.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))) }), _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 })] })] }));
77
+ }
78
+ // One form field row: label + renderer + inline error. Encapsulated as its own
79
+ // component so a STATIC enum select whose options are all gated out by a sibling
80
+ // value (`when`) can hide the ENTIRE row (label included) and run its reset
81
+ // effect with valid hook ordering.
82
+ function FieldRow({ field, value, onChange, values, error, initialValues }) {
83
+ const isStaticSelect = resolveWidget(field) === 'select' && !field.ref && Array.isArray(field.options);
84
+ const effectiveOptions = isStaticSelect
85
+ ? applyOptionWhen(field.options, values, getDependsOn(field))
86
+ : undefined;
87
+ // Reset a selection that the current sibling value no longer permits (e.g.
88
+ // the parent switched away from the value that made this option valid).
89
+ useEffect(() => {
90
+ if (!isStaticSelect || !effectiveOptions)
91
+ return;
92
+ if (value && !effectiveOptions.some((o) => String(o.value) === String(value))) {
93
+ onChange('');
94
+ }
95
+ }, [isStaticSelect, effectiveOptions, value, onChange]);
96
+ // No option applies under the current sibling value → hide the whole field.
97
+ if (isStaticSelect && effectiveOptions && effectiveOptions.length === 0)
98
+ return null;
99
+ const fullWidth = isLineItemsField(field) ||
100
+ resolveWidget(field) === 'textarea' ||
101
+ resolveWidget(field) === 'richtext';
102
+ return (_jsxs("div", { className: 'grid gap-2 ' + (fullWidth ? 'sm:col-span-2' : ''), children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), _jsx(FieldRenderer, { field: field, value: value, onChange: onChange, initialValues: initialValues, values: values, effectiveOptions: effectiveOptions }), error && _jsx("span", { className: "text-red-500 text-sm", role: "alert", children: error })] }));
82
103
  }
83
104
  // seedOptionFromSibling builds a pre-resolved option for an FK field from the
84
105
  // resolved sibling the backend served on the initial record (e.g. a line item's
@@ -105,7 +126,7 @@ function seedOptionFromSibling(field, value, initialValues) {
105
126
  icon: sib.icon,
106
127
  };
107
128
  }
108
- function FieldRenderer({ field, value, onChange, initialValues }) {
129
+ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions, }) {
109
130
  // Repeatable line-items group → render the row grid. Its value is an array
110
131
  // of row objects rather than a scalar.
111
132
  if (isLineItemsField(field)) {
@@ -142,7 +163,7 @@ function FieldRenderer({ field, value, onChange, initialValues }) {
142
163
  case 'color':
143
164
  return _jsx(Input, { id: field.key, type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value) });
144
165
  case 'select':
145
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
166
+ 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)) })] }));
146
167
  case 'switch':
147
168
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
148
169
  case 'number':
@@ -62,6 +62,14 @@ export declare function selectCardColumns(metadata: TableMetadata, maxFields?: n
62
62
  title: ColumnDefinition | null;
63
63
  fields: ColumnDefinition[];
64
64
  };
65
+ /**
66
+ * The value a card cell should render for a column — same resolution the
67
+ * table's cells apply. An FK column (`<rel>_id`) prefers the backend-resolved
68
+ * sibling object (`card.<rel>`, e.g. `{ name }` / `{ value, label }`) over the
69
+ * raw UUID; a zero-UUID FK counts as unset (renders the em-dash). Pure —
70
+ * exported for unit tests.
71
+ */
72
+ export declare function cardCellValue(card: any, col: ColumnDefinition): unknown;
65
73
  /**
66
74
  * Whether a card passes a lane funnel. Picked select/facet `values` match by
67
75
  * equality (IN — the card's field value must be one of them); a free-text
@@ -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":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyF9B,OAAO,EAEH,qBAAqB,EACrB,qBAAqB,EACxB,MAAM,gBAAgB,CAAA;AAUvB,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAIhB,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,CAAA;AAMvD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAejE;AAQD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,wBAAgB,YAAY,CACxB,OAAO,EAAE,GAAG,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAepB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,eAAe,EAAE,GAAG,SAAS,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACX,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAYpB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACpE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAWnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,UAAU,EAAE,OAAO,GACpB,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,aAAa,EACvB,SAAS,SAAI,GACd;IAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkBhE;AAKD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CASvE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,GAAG,EACT,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACzE,OAAO,CAUT;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACxD,MAAM,CAIR;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,GAAG,EACT,IAAI,EAAE,gBAAgB,EAAE,EACxB,KAAK,EAAE,MAAM,GACd,OAAO,CAQT;AAwDD,MAAM,WAAW,kBAAkB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACvC;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,EACR,QAAa,EACb,YAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,cAAc,GACjB,EAAE,kBAAkB,qBAsmCpB"}
@@ -5,14 +5,15 @@ import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDragg
5
5
  import { SortableContext, horizontalListSortingStrategy, useSortable, } from '@dnd-kit/sortable';
6
6
  import { CSS } from '@dnd-kit/utilities';
7
7
  import { arrayMove } from '@dnd-kit/sortable';
8
- import { Calendar, CircleDot, GripVertical, Hash, ListFilter, MoreHorizontal, RotateCcw, Search, Tag, ToggleLeft, Type, X, } from 'lucide-react';
8
+ import { Calendar, CircleDot, GripVertical, Hash, ListFilter, MoreHorizontal, RotateCcw, Search, Settings2, Tag, ToggleLeft, Type, X, } from 'lucide-react';
9
9
  import { toast } from 'sonner';
10
10
  import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, Skeleton, } from '@asteby/metacore-ui/primitives';
11
11
  import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
12
12
  import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
13
13
  import { useApi } from './api-context';
14
14
  import { useStageAutomations, StageAutomationsButton, } from './stage-automations';
15
- import { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, AddStageColumn, CustomStageLaneMenu, CustomStageDialog, CustomStageDeleteDialog, SmartLane, } from './custom-stages';
15
+ import { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, cardMatchesStageFilters, smartLaneParams, AddStageColumn, CustomStageDialog, CustomStageDeleteDialog, StageConfigDialog, SmartLane, } from './custom-stages';
16
+ import { useStageOverrides } from './stage-overrides';
16
17
  import { useDynamicFilters } from './use-dynamic-filters';
17
18
  import { FilterChipsRow, summarizeFilterValues, translateOptionLabels, } from './filter-chips';
18
19
  import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
@@ -20,7 +21,7 @@ import { useMetadataCache } from './metadata-cache';
20
21
  import { ActivityValueRenderer } from './activity-value-renderer';
21
22
  import { DynamicIcon } from './dynamic-icon';
22
23
  import { isColumnVisibleInTable } from './column-visibility';
23
- import { isRowActionVisible } from './dynamic-columns';
24
+ import { isRowActionVisible, relationKeyFor } from './dynamic-columns';
24
25
  import { useCan, usePermissionsActive, resolveRowActions } from './permissions-context';
25
26
  import { useDynamicRowActions } from './dynamic-row-actions';
26
27
  import { useStageLayout } from './stage-layout';
@@ -176,6 +177,27 @@ export function selectCardColumns(metadata, maxFields = 3) {
176
177
  .slice(0, maxFields);
177
178
  return { title, fields };
178
179
  }
180
+ /** The all-zeros UUID — a Go zero-value FK serialized as "set" when it isn't. */
181
+ const ZERO_UUID = /^0{8}-0{4}-0{4}-0{4}-0{12}$/;
182
+ /**
183
+ * The value a card cell should render for a column — same resolution the
184
+ * table's cells apply. An FK column (`<rel>_id`) prefers the backend-resolved
185
+ * sibling object (`card.<rel>`, e.g. `{ name }` / `{ value, label }`) over the
186
+ * raw UUID; a zero-UUID FK counts as unset (renders the em-dash). Pure —
187
+ * exported for unit tests.
188
+ */
189
+ export function cardCellValue(card, col) {
190
+ const raw = card?.[col.key];
191
+ if (typeof raw === 'string' && ZERO_UUID.test(raw))
192
+ return null;
193
+ const relKey = relationKeyFor(col);
194
+ if (relKey !== col.key) {
195
+ const sibling = card?.[relKey];
196
+ if (sibling && typeof sibling === 'object')
197
+ return sibling;
198
+ }
199
+ return raw;
200
+ }
179
201
  /**
180
202
  * Whether a card passes a lane funnel. Picked select/facet `values` match by
181
203
  * equality (IN — the card's field value must be one of them); a free-text
@@ -249,10 +271,16 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
249
271
  // the host has no `/custom-stages` endpoint — the "+ Agregar etapa" column
250
272
  // and lane menus simply don't render.
251
273
  const customStages = useCustomStages(model);
274
+ // Per-org overrides for DECLARED lanes (rename/recolor/conditions). Degrades
275
+ // to no-op when the host has no `/stage-overrides` endpoint — the ⚙ gear then
276
+ // hides on declared lanes (custom lanes keep it via /custom-stages).
277
+ const stageOverrides = useStageOverrides(model);
252
278
  // Dialog state: create/edit a stage, and the delete confirmation.
253
279
  const [stageDialogOpen, setStageDialogOpen] = useState(false);
254
280
  const [editingStage, setEditingStage] = useState(null);
255
281
  const [deletingStage, setDeletingStage] = useState(null);
282
+ // The gear (⚙) "Configurar etapa" dialog — one UI for declared + custom lanes.
283
+ const [configTarget, setConfigTarget] = useState(null);
256
284
  const openCreateStage = useCallback(() => {
257
285
  setEditingStage(null);
258
286
  setStageDialogOpen(true);
@@ -372,6 +400,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
372
400
  const r = (await api.get(endpoint || `/data/${model}`, {
373
401
  params: {
374
402
  ...filterParams,
403
+ ...smartLaneParams(stage.filters),
375
404
  page: 1,
376
405
  per_page: 1,
377
406
  [`f_${gb}`]: stage.key,
@@ -406,6 +435,24 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
406
435
  // the shared records. Scoped by `f_<group_by>=<stage>` on top of the active
407
436
  // filterParams (the stage scope wins over any global group_by filter).
408
437
  const groupByKey = metadata?.group_by || '';
438
+ // Extra per-lane conditions (stage overrides) keyed by stage. A real lane
439
+ // that carries these queries its data — and counts its header — with the
440
+ // stage scope PLUS these filters (serialized like a smart lane's), so the
441
+ // top-up + eager-total requests below layer them on. Sourced from
442
+ // `metadata.stages` (the kernel applies declared + custom-real overrides).
443
+ const stageExtraFilters = useMemo(() => {
444
+ const m = new Map();
445
+ for (const s of metadata?.stages ?? []) {
446
+ if (s.filters && s.filters.length > 0) {
447
+ m.set(s.key, s.filters.map((f) => ({
448
+ field: f.field,
449
+ op: f.op,
450
+ value: f.value,
451
+ })));
452
+ }
453
+ }
454
+ return m;
455
+ }, [metadata?.stages]);
409
456
  const loadMoreLane = useCallback(async (stageKey) => {
410
457
  if (!metadata || !groupByKey)
411
458
  return;
@@ -426,6 +473,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
426
473
  const res = (await api.get(endpoint || `/data/${model}`, {
427
474
  params: {
428
475
  ...filterParams,
476
+ ...smartLaneParams(stageExtraFilters.get(stageKey)),
429
477
  page: nextPage,
430
478
  per_page: lanePageSize,
431
479
  [`f_${groupByKey}`]: stageKey,
@@ -457,7 +505,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
457
505
  },
458
506
  }));
459
507
  }
460
- }, [api, endpoint, model, metadata, groupByKey, filterParams, lanePageSize, lanePagination]);
508
+ }, [api, endpoint, model, metadata, groupByKey, filterParams, lanePageSize, lanePagination, stageExtraFilters]);
461
509
  // Refetch when metadata resolves, on an explicit refresh, or when the
462
510
  // filters change. `fetchData` is stable while `filterParams` is unchanged
463
511
  // (both memoized), so this only re-runs on real input changes. Debounced so
@@ -764,14 +812,82 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
764
812
  const droppableAllowedFor = (stageKey) => !activeId ||
765
813
  stageKey === activeStage ||
766
814
  isTransitionAllowed(transitions, activeStage, stageKey);
815
+ // Opens the gear (⚙) "Configurar etapa" dialog for a lane, routing to the
816
+ // right backend by kind: a custom real stage edits through /custom-stages, a
817
+ // declared stage through /stage-overrides. Seeds the current label/color and
818
+ // any extra conditions the lane already carries.
819
+ const openConfigStage = (stage) => {
820
+ const custom = customStages.available
821
+ ? customByKey.get(stage.key)
822
+ : undefined;
823
+ const raw = stageExtraFilters.get(stage.key) ??
824
+ custom?.filters ??
825
+ [];
826
+ const filters = raw.map((f) => ({
827
+ field: f.field,
828
+ op: f.op,
829
+ value: f.value,
830
+ }));
831
+ if (custom) {
832
+ setConfigTarget({
833
+ kind: 'custom',
834
+ stageKey: stage.key,
835
+ id: custom.id,
836
+ label: custom.label,
837
+ color: custom.color,
838
+ filters,
839
+ customStage: custom,
840
+ });
841
+ }
842
+ else {
843
+ const original = stage.original
844
+ ? {
845
+ label: stage.original.label,
846
+ color: stage.original.color,
847
+ filters: stage.original.filters?.map((f) => ({
848
+ field: f.field,
849
+ op: f.op,
850
+ value: f.value,
851
+ })),
852
+ }
853
+ : undefined;
854
+ setConfigTarget({
855
+ kind: 'declared',
856
+ stageKey: stage.key,
857
+ label: t(stage.label, { defaultValue: stage.label }),
858
+ color: stage.color ?? 'slate',
859
+ filters,
860
+ overridden: !!stage.overridden,
861
+ isFinal: stage.is_final,
862
+ original,
863
+ });
864
+ }
865
+ };
866
+ // Whether a lane offers the gear: custom real stages always (their CRUD is
867
+ // wired); declared lanes only when the host wired /stage-overrides. Never on
868
+ // the synthetic "Sin etapa" lane.
869
+ const isConfigurable = (stage) => {
870
+ if (stage.key === UNASSIGNED_LANE)
871
+ return false;
872
+ return customByKey.has(stage.key)
873
+ ? customStages.available
874
+ : stageOverrides.available;
875
+ };
767
876
  // Builds every `KanbanLane` prop (minus the dnd wiring) for one stage —
768
877
  // shared by the sortable real stages and the plain-droppable unassigned lane.
769
878
  const buildLaneProps = (stage) => {
770
879
  const allCards = grouped.get(stage.key) ?? [];
880
+ // Extra lane conditions (stage override): the server already scopes this
881
+ // lane's top-up + total queries by them, but the shared INITIAL board page
882
+ // is unscoped, so narrow those cards client-side too (belt-and-suspenders).
883
+ const extraFilters = stageExtraFilters.get(stage.key) ?? [];
771
884
  // Per-lane client-side narrowing (instant, scoped to this stage). The
772
885
  // funnel (field/value) and the lane search (query) are AND-combined.
773
886
  const laneFilter = laneFilters[stage.key];
774
887
  let cards = allCards;
888
+ if (extraFilters.length > 0) {
889
+ cards = cards.filter((c) => cardMatchesStageFilters(c, extraFilters));
890
+ }
775
891
  if (laneFilter?.field) {
776
892
  cards = cards.filter((c) => cardMatchesLaneFunnel(c, laneFilter));
777
893
  }
@@ -806,11 +922,9 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
806
922
  onAutomationCreate: automations.create,
807
923
  onAutomationUpdate: automations.update,
808
924
  onAutomationRemove: automations.remove,
809
- customStage: customStages.available
810
- ? customByKey.get(stage.key)
811
- : undefined,
812
- onEditStage: openEditStage,
813
- onDeleteStage: openDeleteStage,
925
+ extraFilters,
926
+ configurable: isConfigurable(stage),
927
+ onConfigure: () => openConfigStage(stage),
814
928
  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, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction }, String(card.id))))),
815
929
  };
816
930
  };
@@ -845,7 +959,40 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
845
959
  }, stage: deletingStage, reassignTargets: stages.map((s) => ({
846
960
  key: s.key,
847
961
  label: s.label,
848
- })), onConfirm: (s, reassignTo) => customStages.remove(s.id, reassignTo) })] }))] }));
962
+ })), onConfirm: (s, reassignTo) => customStages.remove(s.id, reassignTo) })] })), _jsx(StageConfigDialog, { open: !!configTarget, onOpenChange: (o) => {
963
+ if (!o)
964
+ setConfigTarget(null);
965
+ }, columns: metadata?.columns ?? [], target: configTarget, onSaveOverride: async (stageKey, patch) => {
966
+ await stageOverrides.save(stageKey, patch);
967
+ // The override changes the served label/color/filters — refetch
968
+ // metadata so the board repaints, and reload the cards.
969
+ try {
970
+ const res = await api.get(`/metadata/table/${model}`);
971
+ const body = res.data;
972
+ if (body.success) {
973
+ setMetadata(body.data);
974
+ cacheMetadata(model, body.data);
975
+ }
976
+ }
977
+ catch {
978
+ /* keep the current metadata on a refetch miss */
979
+ }
980
+ void fetchData();
981
+ }, onResetOverride: async (stageKey) => {
982
+ await stageOverrides.reset(stageKey);
983
+ try {
984
+ const res = await api.get(`/metadata/table/${model}`);
985
+ const body = res.data;
986
+ if (body.success) {
987
+ setMetadata(body.data);
988
+ cacheMetadata(model, body.data);
989
+ }
990
+ }
991
+ catch {
992
+ /* keep the current metadata on a refetch miss */
993
+ }
994
+ void fetchData();
995
+ }, onUpdateCustom: customStages.update, onDeleteCustom: openDeleteStage })] }));
849
996
  }
850
997
  /**
851
998
  * A per-data-type glyph for the Filtros panel rows (and their popover header):
@@ -933,7 +1080,7 @@ function SortableSmartLane({ droppableDisabled, smartProps, }) {
933
1080
  };
934
1081
  return _jsx(SmartLane, { ...smartProps, dnd: dnd });
935
1082
  }
936
- function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, dnd, model, columns, automationsAvailable, automationRules, onAutomationCreate, onAutomationUpdate, onAutomationRemove, customStage, onEditStage, onDeleteStage, children, }) {
1083
+ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, dnd, model, columns, automationsAvailable, automationRules, onAutomationCreate, onAutomationUpdate, onAutomationRemove, extraFilters, configurable, onConfigure, children, }) {
937
1084
  const { t } = useTranslation();
938
1085
  // Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
939
1086
  // container; a load in flight or an exhausted stage disables it.
@@ -972,6 +1119,23 @@ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMor
972
1119
  text: laneFilter.text,
973
1120
  }
974
1121
  : undefined;
1122
+ // Stage-override conditions active on this lane → a small filter dot in the
1123
+ // header, its `title` listing the conditions (e.g. "priority es igual high").
1124
+ const hasConditions = extraFilters.length > 0;
1125
+ const conditionsSummary = extraFilters
1126
+ .map((f) => {
1127
+ const opLabel = t(`dynamic.custom_stages.op.${f.op}`, {
1128
+ defaultValue: f.op === 'eq'
1129
+ ? 'es igual'
1130
+ : f.op === 'neq'
1131
+ ? 'distinto'
1132
+ : f.op === 'contains'
1133
+ ? 'contiene'
1134
+ : 'en lista',
1135
+ });
1136
+ return `${f.field} ${opLabel} ${f.value}`;
1137
+ })
1138
+ .join(' · ');
975
1139
  return (_jsxs("div", { ref: dnd.setNodeRef,
976
1140
  // Fluid width: lanes grow (flex-1) to fill the board when they all
977
1141
  // fit, capped at max-w so a couple of lanes don't stretch absurdly
@@ -982,9 +1146,17 @@ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMor
982
1146
  outline: dnd.isOver ? '2px solid var(--ring, #3b82f6)' : 'none',
983
1147
  outlineOffset: 2,
984
1148
  ...dnd.style,
985
- }, "data-stage": stage.key, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { ref: dnd.draggable ? dnd.handleRef : undefined, ...(dnd.draggable ? dnd.handleProps : {}), className: `flex min-w-0 items-center gap-1.5 ${dnd.draggable ? 'cursor-grab active:cursor-grabbing' : ''}`, children: [dnd.draggable && (_jsx(GripVertical, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/40 opacity-0 transition-opacity group-hover/lane:opacity-70", "aria-hidden": true })), _jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: formatLaneCount(count, totalCount, serverTotal, laneActive) })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${queryActive ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.searchLane', {
1149
+ }, "data-stage": stage.key, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { ref: dnd.draggable ? dnd.handleRef : undefined, ...(dnd.draggable ? dnd.handleProps : {}), className: `flex min-w-0 items-center gap-1.5 ${dnd.draggable ? 'cursor-grab active:cursor-grabbing' : ''}`, children: [dnd.draggable && (_jsx(GripVertical, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/40 opacity-0 transition-opacity group-hover/lane:opacity-70", "aria-hidden": true })), _jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: formatLaneCount(count, totalCount, serverTotal, laneActive) }), hasConditions && (_jsx("span", { className: "flex items-center text-primary", title: t('dynamic.stage_config.conditions_active', {
1150
+ defaultValue: 'Condiciones: {{list}}',
1151
+ list: conditionsSummary,
1152
+ }), "data-testid": `lane-conditions-${stage.key}`, "aria-label": t('dynamic.stage_config.conditions_active', {
1153
+ defaultValue: 'Condiciones: {{list}}',
1154
+ list: conditionsSummary,
1155
+ }), children: _jsx(ListFilter, { className: "h-3 w-3" }) }))] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${queryActive ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.searchLane', {
986
1156
  defaultValue: 'Buscar en la columna',
987
- }), children: [_jsx(Search, { className: "h-3.5 w-3.5" }), queryActive && (_jsx("span", { className: "absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-primary" }))] }), _jsx(LaneFilterButton, { fields: filterFields, value: funnelValue, onChange: onFunnelChange }), automationsAvailable && (_jsx(StageAutomationsButton, { model: model, stageKey: stage.key, stageLabel: stage.label, columns: columns, rules: automationRules, onCreate: onAutomationCreate, onUpdate: onAutomationUpdate, onRemove: onAutomationRemove })), customStage && onEditStage && onDeleteStage && (_jsx(CustomStageLaneMenu, { stage: customStage, onEdit: onEditStage, onDelete: onDeleteStage }))] })] }), searchOpen && (_jsx("div", { className: "px-3 pb-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchRef, value: laneFilter?.query ?? '', onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
1157
+ }), children: [_jsx(Search, { className: "h-3.5 w-3.5" }), queryActive && (_jsx("span", { className: "absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-primary" }))] }), _jsx(LaneFilterButton, { fields: filterFields, value: funnelValue, onChange: onFunnelChange }), automationsAvailable && (_jsx(StageAutomationsButton, { model: model, stageKey: stage.key, stageLabel: stage.label, columns: columns, rules: automationRules, onCreate: onAutomationCreate, onUpdate: onAutomationUpdate, onRemove: onAutomationRemove })), configurable && (_jsx("button", { type: "button", onClick: onConfigure, className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${hasConditions ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('dynamic.stage_config.open', {
1158
+ defaultValue: 'Configurar etapa',
1159
+ }), "data-testid": `lane-config-${stage.key}`, children: _jsx(Settings2, { className: "h-3.5 w-3.5" }) }))] })] }), searchOpen && (_jsx("div", { className: "px-3 pb-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchRef, value: laneFilter?.query ?? '', onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
988
1160
  if (e.key === 'Escape') {
989
1161
  onQueryChange('');
990
1162
  setSearchOpen(false);
@@ -1063,14 +1235,14 @@ function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, curr
1063
1235
  disabled: !draggable,
1064
1236
  });
1065
1237
  const visibleActions = actions.filter((a) => isRowActionVisible(a, card));
1066
- return (_jsx(Card, { ref: draggable ? setNodeRef : undefined, ...(draggable ? attributes : {}), ...(draggable ? listeners : {}), className: `w-full min-w-0 border-border/70 shadow-sm ${draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`, style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), 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",
1238
+ return (_jsx(Card, { ref: draggable ? setNodeRef : undefined, ...(draggable ? attributes : {}), ...(draggable ? listeners : {}), className: `w-full min-w-0 border-border/70 shadow-sm ${draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`, style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: cardCellValue(card, titleCol), col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), 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",
1067
1239
  // Don't start a drag / card click from the menu button.
1068
1240
  onPointerDown: (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) => (_jsxs(DropdownMenuItem, { onClick: (e) => {
1069
1241
  e.stopPropagation();
1070
1242
  onAction(a.key, card);
1071
- }, children: [_jsx(DynamicIcon, { name: a.icon || 'Zap', className: "mr-2 h-4 w-4" }), a.label] }, a.key))) })] }))] }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 break-words", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
1243
+ }, children: [_jsx(DynamicIcon, { name: a.icon || 'Zap', className: "mr-2 h-4 w-4" }), a.label] }, a.key))) })] }))] }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-xs text-muted-foreground", 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)))] }) }));
1072
1244
  }
1073
1245
  // Static preview rendered inside the DragOverlay (no dnd hooks, no menu).
1074
1246
  function CardPreview({ card, titleCol, fieldCols, locale, timeZone, currency, }) {
1075
- return (_jsx(Card, { className: "w-[284px] cursor-grabbing border-primary/40 shadow-lg", children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsx("div", { className: "break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (String(card.id)) }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 break-words", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
1247
+ return (_jsx(Card, { className: "w-[284px] cursor-grabbing border-primary/40 shadow-lg", children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsx("div", { className: "break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: cardCellValue(card, titleCol), col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (String(card.id)) }), fieldCols.map((col) => (_jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-xs text-muted-foreground", 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)))] }) }));
1076
1248
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAe7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BAmJ/G"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BAmJ/G"}
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
12
12
  import { useEffect, useRef } from 'react';
13
13
  import { Plus, Trash2, Check } from 'lucide-react';
14
- import { resolveWidget, getItemFields, computeLineItemTotals, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, } from './dynamic-form-schema';
14
+ import { resolveWidget, getItemFields, computeLineItemTotals, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
15
15
  import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field';
16
16
  import { useOptionsResolver } from './use-options-resolver';
17
17
  const fmtNumber = (n) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
@@ -95,6 +95,22 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
95
95
  const dependsValue = getDependsOn(field)
96
96
  ? resolveDependsValue(field, formValues, rowValues)
97
97
  : undefined;
98
+ // STATIC enum options gated per-cell by a sibling (row) or header value.
99
+ // Row values win over the header when a key exists in both, matching
100
+ // `resolveDependsValue`. Filtered against each option's `when`.
101
+ const isStaticSelect = widget === 'select' && !field.ref && !getOptionsConfig(field)?.source && Array.isArray(field.options);
102
+ const gateValues = isStaticSelect ? { ...(formValues ?? {}), ...(rowValues ?? {}) } : undefined;
103
+ const effectiveOptions = isStaticSelect
104
+ ? applyOptionWhen(field.options, gateValues, getDependsOn(field))
105
+ : undefined;
106
+ // Reset a selection the current sibling value no longer permits.
107
+ useEffect(() => {
108
+ if (!isStaticSelect || !effectiveOptions)
109
+ return;
110
+ if (value && !effectiveOptions.some((o) => String(o.value) === String(value))) {
111
+ onChange('');
112
+ }
113
+ }, [isStaticSelect, effectiveOptions, value, onChange]);
98
114
  // Async searchable picker per row cell — e.g. the account_id column of a
99
115
  // journal entry's debit/credit lines. Same widget as the flat form.
100
116
  if (widget === 'dynamic_select') {
@@ -109,8 +125,13 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
109
125
  return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, rows: 2 }));
110
126
  case 'color':
111
127
  return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: off }));
112
- case 'select':
113
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
128
+ case 'select': {
129
+ const opts = effectiveOptions ?? field.options;
130
+ // No option applies under the current sibling value → hide the cell.
131
+ if (effectiveOptions && effectiveOptions.length === 0)
132
+ return null;
133
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, 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))) })] }));
134
+ }
114
135
  case 'switch':
115
136
  return _jsx(Switch, { checked: !!value, onCheckedChange: onChange, disabled: off });
116
137
  case 'number':
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BAyiCnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BA6iCnB"}
@@ -93,6 +93,11 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
93
93
  return value;
94
94
  const prefix = value.substring(0, colonIdx).toLowerCase();
95
95
  const rest = value.substring(colonIdx + 1);
96
+ // `eq:` is the wire's explicit equality operator, but internally a
97
+ // select stores the bare value — unwrap it so `f_status=eq:reception`
98
+ // matches the option "reception" (header filter + chip label).
99
+ if (prefix === 'eq')
100
+ return rest;
96
101
  const operator = urlAliasToOperator[prefix];
97
102
  return operator ? `${operator}:${rest}` : value;
98
103
  };
package/dist/index.d.ts CHANGED
@@ -3,8 +3,9 @@ export * from './options-context';
3
3
  export * from './dynamic-table';
4
4
  export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
5
5
  export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, type StageAutomation, type StageAutomationAction, type StageAutomationActionType, type NewStageAutomation, type UseStageAutomationsResult, type StageAutomationsButtonProps, } from './stage-automations';
6
- export { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, smartLaneParams, customStageFilterFields, isCustomStageDraftValid, slugifyStageKey, emptyCustomStageFilter, AddStageColumn, CustomStageLaneMenu, CustomStageDialog, CustomStageDeleteDialog, SmartLane, CUSTOM_STAGE_COLORS, CUSTOM_STAGE_FILTER_OPS, type CustomStage, type NewCustomStage, type CustomStageType, type CustomStageFilter, type CustomStageFilterOp, type UseCustomStagesResult, } from './custom-stages';
6
+ export { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, smartLaneParams, cardMatchesStageFilters, customStageFilterFields, isCustomStageDraftValid, slugifyStageKey, emptyCustomStageFilter, AddStageColumn, CustomStageLaneMenu, CustomStageDialog, CustomStageDeleteDialog, StageConditionBuilder, StageConfigDialog, stageFilterOpSymbol, SmartLane, CUSTOM_STAGE_COLORS, CUSTOM_STAGE_FILTER_OPS, type CustomStage, type NewCustomStage, type CustomStageType, type CustomStageFilter, type CustomStageFilterOp, type UseCustomStagesResult, type StageConfigTarget, type StageConfigKind, type StageConditionBuilderProps, type StageConfigDialogProps, } from './custom-stages';
7
7
  export { useStageLayout, type StageLayout, type UseStageLayoutResult, } from './stage-layout';
8
+ export { useStageOverrides, type StageOverridePatch, type UseStageOverridesResult, } from './stage-overrides';
8
9
  export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
9
10
  export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
10
11
  export * from './dynamic-form';