@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.
- package/CHANGELOG.md +46 -0
- package/dist/custom-stages.d.ts +96 -1
- package/dist/custom-stages.d.ts.map +1 -1
- package/dist/custom-stages.js +259 -24
- package/dist/dynamic-form-schema.d.ts +18 -1
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +43 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +30 -9
- package/dist/dynamic-kanban.d.ts +8 -0
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +188 -16
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +24 -3
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/stage-overrides.d.ts +25 -0
- package/dist/stage-overrides.d.ts.map +1 -0
- package/dist/stage-overrides.js +64 -0
- package/dist/types.d.ts +69 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/dependent-enum-form.test.tsx +84 -0
- package/src/__tests__/dependent-enum-options.test.ts +67 -0
- package/src/__tests__/dynamic-kanban.test.tsx +75 -0
- package/src/__tests__/stage-overrides.test.tsx +293 -0
- package/src/custom-stages.tsx +716 -109
- package/src/dynamic-form-schema.ts +44 -1
- package/src/dynamic-form.tsx +76 -28
- package/src/dynamic-kanban.tsx +236 -25
- package/src/dynamic-line-items.tsx +26 -2
- package/src/dynamic-table.tsx +4 -0
- package/src/index.ts +13 -0
- package/src/stage-overrides.ts +95 -0
- 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,
|
|
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"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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':
|
package/dist/dynamic-kanban.d.ts
CHANGED
|
@@ -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":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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,
|
|
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) })
|
|
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 })),
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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,+
|
|
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"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -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';
|