@asteby/metacore-runtime-react 23.7.0 → 23.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a3d3fdf: DynamicKanban: reordenar las columnas del tablero arrastrándolas por su encabezado (estilo Trello/Bitrix). Aplica a todas las lanes —etapas declaradas, etapas custom y smart lanes— con reorden optimista y persistencia por organización vía el endpoint `/stage-layout` del host (`GET`/`PUT` orden completo/`DELETE` restablece). Si el host no expone el endpoint, el drag de columnas queda deshabilitado en silencio y el tablero funciona igual. Se agrega el hook `useStageLayout` y un affordance "Restablecer orden" cuando existe un orden custom.
8
+
9
+ ## 23.7.1
10
+
11
+ ### Patch Changes
12
+
13
+ - a3538da: fix(runtime-react): translate addon action labels at render (toolbar + modal)
14
+
15
+ `ModelActionToolbar` and `ActionModalDispatcher` rendered an action's `label`
16
+ (and each field's `label`) verbatim. For an addon's custom action these are i18n
17
+ keys (e.g. `integration_github.action.create_issue.label`) whose locale bundle
18
+ loads asynchronously, so the create button, the modal title/submit, and the
19
+ field labels showed the raw key — and never re-derived once the bundle landed.
20
+ They now translate at render with `t(label, { defaultValue: label })`, so the
21
+ label resolves the moment the addon i18n arrives (via the i18next store `added`
22
+ event) and an already-localized string passes through unchanged.
23
+
3
24
  ## 23.7.0
4
25
 
5
26
  ### Minor Changes
@@ -111,6 +111,10 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
111
111
  const { t } = useTranslation();
112
112
  const api = useApi();
113
113
  const [executing, setExecuting] = useState(false);
114
+ // `action.label` is an addon-contributed i18n key; its locale bundle loads
115
+ // asynchronously, so translate at render (defaultValue keeps an already
116
+ // localized label unchanged).
117
+ const label = t(action.label, { defaultValue: action.label });
114
118
  const execute = async () => {
115
119
  setExecuting(true);
116
120
  try {
@@ -132,10 +136,14 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
132
136
  setExecuting(false);
133
137
  }
134
138
  };
135
- return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${action.label}?` })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
139
+ return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
136
140
  }
137
141
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
138
142
  const { t } = useTranslation();
143
+ // Addon-contributed labels (action + fields) are i18n keys whose locale
144
+ // bundle loads asynchronously; translate at render so they don't render raw.
145
+ // defaultValue keeps an already-localized string unchanged.
146
+ const tl = (s) => t(s, { defaultValue: s });
139
147
  const api = useApi();
140
148
  const [formData, setFormData] = useState({});
141
149
  const [executing, setExecuting] = useState(false);
@@ -194,13 +202,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
194
202
  if (isLineItemsField(field)) {
195
203
  const rows = formData[field.key];
196
204
  if (!Array.isArray(rows) || rows.length === 0) {
197
- toast.error(`${field.label} requiere al menos un renglón`);
205
+ toast.error(`${tl(field.label)} requiere al menos un renglón`);
198
206
  return;
199
207
  }
200
208
  continue;
201
209
  }
202
210
  if (!formData[field.key] && formData[field.key] !== false) {
203
- toast.error(`${field.label} es requerido`);
211
+ toast.error(`${tl(field.label)} es requerido`);
204
212
  return;
205
213
  }
206
214
  }
@@ -239,12 +247,12 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
239
247
  : hasLineItems
240
248
  ? '820px'
241
249
  : undefined;
242
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90vh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90vh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
250
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90vh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90vh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
243
251
  const fullWidth = isLineItemsField(field) ||
244
252
  resolveWidget(field) === 'textarea' ||
245
253
  resolveWidget(field) === 'richtext';
246
- return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: field.label }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData)] }, field.key));
247
- }), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
254
+ return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData)] }, field.key));
255
+ }), relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] })] }) }));
248
256
  }
249
257
  function renderField(field, value, onChange,
250
258
  // Full current form values — lets a line-items grid (and any cascading
@@ -159,6 +159,18 @@ export interface SmartLaneProps {
159
159
  refreshTrigger?: any;
160
160
  onEdit: (stage: CustomStage) => void;
161
161
  onDelete: (stage: CustomStage) => void;
162
+ /**
163
+ * Optional drag-and-drop wiring (from the kanban's sortable wrapper) so a
164
+ * smart lane can be reordered by its header like a real stage. Absent → the
165
+ * lane is static.
166
+ */
167
+ dnd?: {
168
+ setNodeRef: (el: HTMLElement | null) => void;
169
+ style?: React.CSSProperties;
170
+ isDragging?: boolean;
171
+ handleRef?: (el: HTMLElement | null) => void;
172
+ handleProps?: Record<string, any>;
173
+ };
162
174
  }
163
175
  /**
164
176
  * A virtual lane defined by `filters`. It runs its OWN list query (the board's
@@ -166,6 +178,6 @@ export interface SmartLaneProps {
166
178
  * main board page loaded. Cards render read-only — a smart lane is a saved view,
167
179
  * not a drop target — and the header carries a funnel glyph + the custom menu.
168
180
  */
169
- export declare function SmartLane({ stage, model, endpoint, defaultFilters, pageSize, isDark, renderCard, refreshTrigger, onEdit, onDelete, }: SmartLaneProps): React.JSX.Element;
181
+ export declare function SmartLane({ stage, model, endpoint, defaultFilters, pageSize, isDark, renderCard, refreshTrigger, onEdit, onDelete, dnd, }: SmartLaneProps): React.JSX.Element;
170
182
  export type { ApiClient };
171
183
  //# sourceMappingURL=custom-stages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"custom-stages.d.ts","sourceRoot":"","sources":["../src/custom-stages.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"custom-stages.d.ts","sourceRoot":"","sources":["../src/custom-stages.tsx"],"names":[],"mappings":"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"}
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
20
20
  // All UI text goes through t() with a Spanish defaultValue.
21
21
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
22
  import { useTranslation } from 'react-i18next';
23
- import { Plus, Trash2, Pencil, MoreVertical, Filter, X } from 'lucide-react';
23
+ import { Plus, Trash2, Pencil, MoreVertical, Filter, GripVertical, X } from 'lucide-react';
24
24
  import { toast } from 'sonner';
25
25
  import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, } from '@asteby/metacore-ui/primitives';
26
26
  import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
@@ -484,7 +484,7 @@ export function CustomStageDeleteDialog({ open, onOpenChange, stage, reassignTar
484
484
  * main board page loaded. Cards render read-only — a smart lane is a saved view,
485
485
  * not a drop target — and the header carries a funnel glyph + the custom menu.
486
486
  */
487
- export function SmartLane({ stage, model, endpoint, defaultFilters, pageSize = 50, isDark, renderCard, refreshTrigger, onEdit, onDelete, }) {
487
+ export function SmartLane({ stage, model, endpoint, defaultFilters, pageSize = 50, isDark, renderCard, refreshTrigger, onEdit, onDelete, dnd, }) {
488
488
  const { t } = useTranslation();
489
489
  const api = useApi();
490
490
  const [records, setRecords] = useState([]);
@@ -527,7 +527,7 @@ export function SmartLane({ stage, model, endpoint, defaultFilters, pageSize = 5
527
527
  isDark,
528
528
  });
529
529
  const count = total ?? records.length;
530
- return (_jsxs("div", { className: "flex min-w-[280px] max-w-[420px] flex-1 shrink-0 flex-col rounded-xl border border-dashed bg-muted/20", "data-smart-stage": stage.key, "data-testid": `smart-lane-${stage.key}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-muted-foreground", title: t('dynamic.custom_stages.smart_hint', {
530
+ return (_jsxs("div", { ref: dnd?.setNodeRef, className: "group/lane flex min-w-[280px] max-w-[420px] flex-1 shrink-0 flex-col rounded-xl border border-dashed bg-muted/20", style: { opacity: dnd?.isDragging ? 0.6 : 1, ...dnd?.style }, "data-smart-stage": stage.key, "data-testid": `smart-lane-${stage.key}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { ref: dnd ? dnd.handleRef : undefined, ...(dnd ? dnd.handleProps : {}), className: `flex min-w-0 items-center gap-1.5 ${dnd ? 'cursor-grab active:cursor-grabbing' : ''}`, children: [dnd && (_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-muted-foreground", title: t('dynamic.custom_stages.smart_hint', {
531
531
  defaultValue: 'Etapa inteligente (por condiciones)',
532
532
  }), children: _jsx(Filter, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: count })] }), _jsx(CustomStageLaneMenu, { stage: stage, onEdit: onEdit, onDelete: onDelete })] }), _jsx("div", { className: "flex min-h-[55vh] max-h-[70vh] min-w-0 flex-col gap-2 overflow-y-auto px-2 pb-3", children: loading && records.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : records.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('dynamic.custom_stages.smart_empty', {
533
533
  defaultValue: 'Sin tarjetas que cumplan las condiciones',
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -2,7 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDraggable, useDroppable, } from '@dnd-kit/core';
5
- import { Calendar, CircleDot, Hash, ListFilter, MoreHorizontal, Search, Tag, ToggleLeft, Type, X, } from 'lucide-react';
5
+ import { SortableContext, horizontalListSortingStrategy, useSortable, } from '@dnd-kit/sortable';
6
+ import { CSS } from '@dnd-kit/utilities';
7
+ import { arrayMove } from '@dnd-kit/sortable';
8
+ import { Calendar, CircleDot, GripVertical, Hash, ListFilter, MoreHorizontal, RotateCcw, Search, Tag, ToggleLeft, Type, X, } from 'lucide-react';
6
9
  import { toast } from 'sonner';
7
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';
8
11
  import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
@@ -20,6 +23,7 @@ import { isColumnVisibleInTable } from './column-visibility';
20
23
  import { isRowActionVisible } from './dynamic-columns';
21
24
  import { useCan, usePermissionsActive, resolveRowActions } from './permissions-context';
22
25
  import { useDynamicRowActions } from './dynamic-row-actions';
26
+ import { useStageLayout } from './stage-layout';
23
27
  // Re-exported for tests + backward-compat: these live in ./filter-chips now
24
28
  // (shared with DynamicTable) but were historically imported from here.
25
29
  export { summarizeFilterValues, translateOptionLabels };
@@ -274,6 +278,18 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
274
278
  const [lanePagination, setLanePagination] = useState({});
275
279
  // Active drag card id (for the DragOverlay + drop-zone highlighting).
276
280
  const [activeId, setActiveId] = useState(null);
281
+ // Active drag LANE id — a header drag reorders columns (Trello/Bitrix-style)
282
+ // rather than moving a card. Kept apart so onDragEnd routes by draggable type.
283
+ const [activeLaneId, setActiveLaneId] = useState(null);
284
+ // Per-org lane order. `useStageLayout` reports whether the host wired the
285
+ // `/stage-layout` endpoint (→ lane drag turns on) and persists the chosen
286
+ // order. `laneOrderOverride` is the OPTIMISTIC session order applied on top of
287
+ // the metadata (the backend also stamps `stages[]/smart_lanes[].order`, so the
288
+ // board already paints ordered on load — this only backs the live drag + the
289
+ // revert-on-failure). Null → follow the metadata order.
290
+ const stageLayout = useStageLayout(model);
291
+ const [laneOrderOverride, setLaneOrderOverride] = useState(null);
292
+ const laneReorderEnabled = stageLayout.available;
277
293
  // Monotonic token for the current board load. Bumped on every fetchData so a
278
294
  // slow eager-totals response from a superseded filter set can't inject stale
279
295
  // stage totals into the fresh board.
@@ -541,6 +557,44 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
541
557
  return Math.max(base, ...positions, 0) + 1;
542
558
  }, [customStages.stages, stages.length, smartStages.length]);
543
559
  const transitions = metadata?.transitions;
560
+ // Unified, ordered list of the DRAGGABLE lanes (real stages + smart lanes) —
561
+ // the sortable sequence the header drag reorders. Real stages and smart lanes
562
+ // are merged by their `order` (the backend stamps a global sequence once a
563
+ // custom order exists); a `laneOrderOverride` from the live drag wins. Ties
564
+ // (the default, un-customized case where both sets start at 0) keep stages
565
+ // before smart lanes, matching the pre-reorder layout.
566
+ const renderLanes = useMemo(() => {
567
+ const arr = [
568
+ ...stages.map((s, i) => ({
569
+ kind: 'stage',
570
+ stage: s,
571
+ order: s.order ?? i,
572
+ })),
573
+ ...smartStages.map((s, i) => ({
574
+ kind: 'smart',
575
+ stage: s,
576
+ order: s.position ?? 1000 + i,
577
+ })),
578
+ ];
579
+ if (laneOrderOverride) {
580
+ const idx = new Map(laneOrderOverride.map((k, i) => [k, i]));
581
+ arr.sort((a, b) => (idx.get(a.stage.key) ?? Number.MAX_SAFE_INTEGER) -
582
+ (idx.get(b.stage.key) ?? Number.MAX_SAFE_INTEGER));
583
+ }
584
+ else {
585
+ arr.sort((a, b) => a.order - b.order ||
586
+ (a.kind === b.kind ? 0 : a.kind === 'stage' ? -1 : 1));
587
+ }
588
+ return arr.map(({ kind, stage: s }) => kind === 'stage'
589
+ ? { kind: 'stage', stage: s }
590
+ : { kind: 'smart', stage: s });
591
+ }, [stages, smartStages, laneOrderOverride]);
592
+ // The sortable ids, in current visual order — SortableContext items + the
593
+ // basis for the arrayMove the drop computes.
594
+ const boardLaneKeys = useMemo(() => renderLanes.map((l) => l.stage.key), [renderLanes]);
595
+ // Real (card-droppable) stage keys — guards a card drop from ever landing on
596
+ // a smart lane (a saved view, never a stored stage value).
597
+ const realStageKeys = useMemo(() => new Set(stages.map((s) => s.key)), [stages]);
544
598
  const grouped = useMemo(() => groupByStage(records, groupByKey, stages), [records, groupByKey, stages]);
545
599
  const { title: titleCol, fields: fieldCols } = useMemo(() => (metadata ? selectCardColumns(metadata) : { title: null, fields: [] }), [metadata]);
546
600
  // Columns the lane search scans: the card title + its visible field cells.
@@ -576,15 +630,51 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
576
630
  return raw === null || raw === undefined ? '' : String(raw);
577
631
  }, [cardById, groupByKey]);
578
632
  const onDragStart = useCallback((e) => {
579
- setActiveId(String(e.active.id));
633
+ if (e.active.data.current?.type === 'lane') {
634
+ setActiveLaneId(String(e.active.id));
635
+ }
636
+ else {
637
+ setActiveId(String(e.active.id));
638
+ }
580
639
  }, []);
640
+ // Optimistic lane reorder: reorder the columns in local state immediately,
641
+ // PUT the full new order, and revert + toast on failure. Keys mix real
642
+ // stages and smart lanes (the backend applies the order to both).
643
+ const reorderLanes = useCallback(async (activeKey, overKey) => {
644
+ const from = boardLaneKeys.indexOf(activeKey);
645
+ const to = boardLaneKeys.indexOf(overKey);
646
+ if (from === -1 || to === -1 || from === to)
647
+ return;
648
+ const next = arrayMove(boardLaneKeys, from, to);
649
+ const prev = laneOrderOverride;
650
+ setLaneOrderOverride(next);
651
+ try {
652
+ await stageLayout.save(next);
653
+ }
654
+ catch {
655
+ setLaneOrderOverride(prev);
656
+ toast.error(t('dynamic.stage_layout.save_error', {
657
+ defaultValue: 'No se pudo guardar el orden',
658
+ }));
659
+ }
660
+ }, [boardLaneKeys, laneOrderOverride, stageLayout, t]);
581
661
  const onDragEnd = useCallback(async (e) => {
582
662
  setActiveId(null);
663
+ setActiveLaneId(null);
583
664
  const { active, over } = e;
665
+ // A header drag reorders columns rather than moving a card.
666
+ if (active.data.current?.type === 'lane') {
667
+ if (over)
668
+ await reorderLanes(String(active.id), String(over.id));
669
+ return;
670
+ }
584
671
  if (!over)
585
672
  return;
586
673
  const cardId = String(active.id);
587
674
  const destStage = String(over.id);
675
+ // Never drop a card onto a smart lane (a saved view, not a stage).
676
+ if (!realStageKeys.has(destStage) && destStage !== UNASSIGNED_LANE)
677
+ return;
588
678
  const srcStage = stageOfCard(cardId);
589
679
  if (srcStage === destStage)
590
680
  return;
@@ -626,7 +716,29 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
626
716
  ? `: ${err.response.data.message}`
627
717
  : ''));
628
718
  }
629
- }, [api, endpoint, groupByKey, lanePagination, model, records, stageOfCard, t, transitions]);
719
+ }, [api, endpoint, groupByKey, lanePagination, model, records, stageOfCard, t, transitions, realStageKeys, reorderLanes]);
720
+ // Board-level "Restablecer orden": drop the stored order and refetch the
721
+ // metadata so the lanes fall back to the DECLARED order. Optimistic — reverts
722
+ // its local override on failure.
723
+ const resetLaneOrder = useCallback(async () => {
724
+ const prev = laneOrderOverride;
725
+ setLaneOrderOverride(null);
726
+ try {
727
+ await stageLayout.reset();
728
+ const res = await api.get(`/metadata/table/${model}`);
729
+ const body = res.data;
730
+ if (body.success) {
731
+ setMetadata(body.data);
732
+ cacheMetadata(model, body.data);
733
+ }
734
+ }
735
+ catch {
736
+ setLaneOrderOverride(prev);
737
+ toast.error(t('dynamic.stage_layout.reset_error', {
738
+ defaultValue: 'No se pudo restablecer el orden',
739
+ }));
740
+ }
741
+ }, [api, cacheMetadata, laneOrderOverride, model, stageLayout, t]);
630
742
  if (loading) {
631
743
  return (_jsx("div", { className: "flex w-full gap-4 overflow-x-auto p-1", children: [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "min-w-[280px] max-w-[420px] flex-1 shrink-0 space-y-3", children: [_jsx(Skeleton, { className: "h-8 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" })] }, i))) }));
632
744
  }
@@ -637,15 +749,85 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
637
749
  }
638
750
  const activeCard = activeId ? cardById.get(activeId) : null;
639
751
  const activeStage = activeId ? stageOfCard(activeId) : '';
640
- const lanes = [...stages];
641
- if (grouped.has(UNASSIGNED_LANE)) {
642
- lanes.push({
752
+ // The synthetic "Sin etapa" lane (only when some record's stage matches no
753
+ // declared lane). Rendered after the sortable lanes, never draggable itself.
754
+ const unassignedStage = grouped.has(UNASSIGNED_LANE)
755
+ ? {
643
756
  key: UNASSIGNED_LANE,
644
757
  label: t('kanban.unassigned', { defaultValue: 'Sin etapa' }),
645
758
  color: 'slate',
646
759
  order: Number.MAX_SAFE_INTEGER,
647
- });
648
- }
760
+ }
761
+ : null;
762
+ // Whether a card may drop into a lane given the active card's stage + the
763
+ // declared transitions (a same-stage or unrestricted move always passes).
764
+ const droppableAllowedFor = (stageKey) => !activeId ||
765
+ stageKey === activeStage ||
766
+ isTransitionAllowed(transitions, activeStage, stageKey);
767
+ // Builds every `KanbanLane` prop (minus the dnd wiring) for one stage —
768
+ // shared by the sortable real stages and the plain-droppable unassigned lane.
769
+ const buildLaneProps = (stage) => {
770
+ const allCards = grouped.get(stage.key) ?? [];
771
+ // Per-lane client-side narrowing (instant, scoped to this stage). The
772
+ // funnel (field/value) and the lane search (query) are AND-combined.
773
+ const laneFilter = laneFilters[stage.key];
774
+ let cards = allCards;
775
+ if (laneFilter?.field) {
776
+ cards = cards.filter((c) => cardMatchesLaneFunnel(c, laneFilter));
777
+ }
778
+ if (laneFilter?.query?.trim()) {
779
+ cards = cards.filter((c) => cardMatchesLaneQuery(c, searchCols, laneFilter.query));
780
+ }
781
+ const laneState = lanePagination[stage.key];
782
+ const isUnassigned = stage.key === UNASSIGNED_LANE;
783
+ const laneHasMore = !isUnassigned && !laneState?.done;
784
+ return {
785
+ stage,
786
+ count: cards.length,
787
+ totalCount: allCards.length,
788
+ serverTotal: laneState?.total ?? null,
789
+ hasMore: laneHasMore,
790
+ loadingMore: !!laneState?.loading,
791
+ onLoadMore: () => loadMoreLane(stage.key),
792
+ filterFields,
793
+ laneFilter,
794
+ onFunnelChange: (f) => updateLaneFilter(stage.key, {
795
+ field: f?.field,
796
+ values: f?.values,
797
+ text: f?.text,
798
+ }),
799
+ onQueryChange: (q) => updateLaneFilter(stage.key, { query: q }),
800
+ isDark,
801
+ dimmed: !!activeId && !droppableAllowedFor(stage.key),
802
+ model,
803
+ columns: metadata?.columns ?? [],
804
+ automationsAvailable: automations.available && stage.key !== UNASSIGNED_LANE,
805
+ automationRules: automations.byStage.get(stage.key) ?? [],
806
+ onAutomationCreate: automations.create,
807
+ onAutomationUpdate: automations.update,
808
+ onAutomationRemove: automations.remove,
809
+ customStage: customStages.available
810
+ ? customByKey.get(stage.key)
811
+ : undefined,
812
+ onEditStage: openEditStage,
813
+ onDeleteStage: openDeleteStage,
814
+ 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
+ };
816
+ };
817
+ // Props for one smart (virtual) lane — a read-only, filter-defined column.
818
+ const buildSmartProps = (smart) => ({
819
+ stage: smart,
820
+ model,
821
+ endpoint,
822
+ defaultFilters,
823
+ pageSize,
824
+ isDark,
825
+ refreshTrigger,
826
+ onEdit: openEditStage,
827
+ onDelete: openDeleteStage,
828
+ renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })),
829
+ });
830
+ const showResetOrder = laneReorderEnabled && (stageLayout.hasCustomLayout || !!laneOrderOverride);
649
831
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", "data-testid": "kanban-filters", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t('kanban.searchPlaceholder', { defaultValue: 'Buscar...' }), className: "h-8 w-52 pl-8 text-sm" })] }), filterFields.length > 0 && (_jsxs(Sheet, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", children: [_jsx(ListFilter, { className: "h-3.5 w-3.5" }), t('kanban.filters', { defaultValue: 'Filtros' }), activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-0.5 h-4 min-w-4 justify-center rounded-full px-1 text-[10px] tabular-nums", children: activeFilterCount }))] }) }), _jsxs(SheetContent, { side: "right", className: "flex w-80 flex-col gap-0 p-0 sm:max-w-sm", children: [_jsx(SheetHeader, { className: "space-y-0 border-b px-4 py-3", children: _jsx(SheetTitle, { className: "text-sm", children: t('kanban.filters', { defaultValue: 'Filtros' }) }) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1 overflow-y-auto px-4 py-3", children: [activeFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "px-0.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: t('kanban.activeFilters', {
650
832
  defaultValue: 'Con filtros activos',
651
833
  }) }), activeFields.map((field) => (_jsx(SheetFilterRow, { field: field, isStage: field.key === groupByKey }, field.key))), inactiveFields.length > 0 && (_jsx("div", { className: "my-2 border-t" }))] })), inactiveFields.map((field) => (_jsx(SheetFilterRow, { field: field, isStage: field.key === groupByKey }, field.key)))] }), _jsxs("div", { className: "sticky bottom-0 flex items-center justify-between gap-2 border-t bg-background px-4 py-3", children: [_jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: activeFilterCount > 0
@@ -655,35 +837,9 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
655
837
  })
656
838
  : t('kanban.noActiveFilters', {
657
839
  defaultValue: 'Sin filtros',
658
- }) }), _jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 gap-1 text-xs", onClick: clearAll, disabled: activeFilterCount === 0, children: [_jsx(X, { className: "h-3.5 w-3.5" }), t('kanban.clearAll', { defaultValue: 'Limpiar todo' }), activeFilterCount > 0 ? ` (${activeFilterCount})` : ''] })] })] })] }))] }), _jsx(FilterChipsRow, { fields: activeFields, onClearAll: clearAll, "data-testid": "kanban-filter-chips" }), _jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsxs("div", { className: "flex w-full min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: [lanes.map((stage) => {
659
- const allCards = grouped.get(stage.key) ?? [];
660
- // Per-lane client-side narrowing (instant, scoped to this
661
- // stage). The funnel (field/value) and the lane search
662
- // (query) are AND-combined.
663
- const laneFilter = laneFilters[stage.key];
664
- let cards = allCards;
665
- if (laneFilter?.field) {
666
- cards = cards.filter((c) => cardMatchesLaneFunnel(c, laneFilter));
667
- }
668
- if (laneFilter?.query?.trim()) {
669
- cards = cards.filter((c) => cardMatchesLaneQuery(c, searchCols, laneFilter.query));
670
- }
671
- const droppableAllowed = !activeId ||
672
- stage.key === activeStage ||
673
- isTransitionAllowed(transitions, activeStage, stage.key);
674
- // Infinite scroll is per declared stage; the synthetic
675
- // "unassigned" lane can't be stage-scoped, so it never tops up.
676
- const laneState = lanePagination[stage.key];
677
- const isUnassigned = stage.key === UNASSIGNED_LANE;
678
- const laneHasMore = !isUnassigned && !laneState?.done;
679
- return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, serverTotal: laneState?.total ?? null, hasMore: laneHasMore, loadingMore: !!laneState?.loading, onLoadMore: () => loadMoreLane(stage.key), filterFields: filterFields, laneFilter: laneFilter, onFunnelChange: (f) => updateLaneFilter(stage.key, {
680
- field: f?.field,
681
- values: f?.values,
682
- text: f?.text,
683
- }), onQueryChange: (q) => updateLaneFilter(stage.key, { query: q }), isDark: isDark, dimmed: !!activeId && !droppableAllowed, disabled: !!activeId && !droppableAllowed, model: model, columns: metadata?.columns ?? [], automationsAvailable: automations.available && stage.key !== UNASSIGNED_LANE, automationRules: automations.byStage.get(stage.key) ?? [], onAutomationCreate: automations.create, onAutomationUpdate: automations.update, onAutomationRemove: automations.remove, customStage: customStages.available
684
- ? customByKey.get(stage.key)
685
- : undefined, onEditStage: openEditStage, onDeleteStage: openDeleteStage, 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))))) }, stage.key));
686
- }), smartStages.map((smart) => (_jsx(SmartLane, { stage: smart, model: model, endpoint: endpoint, defaultFilters: defaultFilters, pageSize: pageSize, isDark: isDark, refreshTrigger: refreshTrigger, onEdit: openEditStage, onDelete: openDeleteStage, renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })) }, `smart-${smart.key}`))), customStages.available && (_jsx(AddStageColumn, { onClick: openCreateStage }))] }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] }), customStages.available && (_jsxs(_Fragment, { children: [_jsx(CustomStageDialog, { open: stageDialogOpen, onOpenChange: setStageDialogOpen, model: model, columns: metadata?.columns ?? [], initial: editingStage, nextPosition: nextStagePosition, onCreate: customStages.create, onUpdate: customStages.update }), _jsx(CustomStageDeleteDialog, { open: !!deletingStage, onOpenChange: (o) => {
840
+ }) }), _jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 gap-1 text-xs", onClick: clearAll, disabled: activeFilterCount === 0, children: [_jsx(X, { className: "h-3.5 w-3.5" }), t('kanban.clearAll', { defaultValue: 'Limpiar todo' }), activeFilterCount > 0 ? ` (${activeFilterCount})` : ''] })] })] })] })), showResetOrder && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 gap-1.5 text-xs text-muted-foreground", onClick: () => void resetLaneOrder(), "data-testid": "kanban-reset-order", children: [_jsx(RotateCcw, { className: "h-3.5 w-3.5" }), t('dynamic.stage_layout.reset', {
841
+ defaultValue: 'Restablecer orden',
842
+ })] }))] }), _jsx(FilterChipsRow, { fields: activeFields, onClearAll: clearAll, "data-testid": "kanban-filter-chips" }), _jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx(SortableContext, { items: boardLaneKeys, strategy: horizontalListSortingStrategy, children: _jsxs("div", { className: "flex w-full min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: [renderLanes.map((lane) => lane.kind === 'stage' ? (_jsx(SortableStageLane, { reorderEnabled: laneReorderEnabled, droppableDisabled: !!activeId && !droppableAllowedFor(lane.stage.key), laneProps: buildLaneProps(lane.stage) }, lane.stage.key)) : laneReorderEnabled ? (_jsx(SortableSmartLane, { droppableDisabled: !!activeId, smartProps: buildSmartProps(lane.stage) }, `smart-${lane.stage.key}`)) : (_jsx(SmartLane, { ...buildSmartProps(lane.stage) }, `smart-${lane.stage.key}`))), unassignedStage && (_jsx(DroppableStageLane, { droppableDisabled: !!activeId && !droppableAllowedFor(UNASSIGNED_LANE), laneProps: buildLaneProps(unassignedStage) }, UNASSIGNED_LANE)), customStages.available && (_jsx(AddStageColumn, { onClick: openCreateStage }))] }) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] }), customStages.available && (_jsxs(_Fragment, { children: [_jsx(CustomStageDialog, { open: stageDialogOpen, onOpenChange: setStageDialogOpen, model: model, columns: metadata?.columns ?? [], initial: editingStage, nextPosition: nextStagePosition, onCreate: customStages.create, onUpdate: customStages.update }), _jsx(CustomStageDeleteDialog, { open: !!deletingStage, onOpenChange: (o) => {
687
843
  if (!o)
688
844
  setDeletingStage(null);
689
845
  }, stage: deletingStage, reassignTargets: stages.map((s) => ({
@@ -718,9 +874,67 @@ function SheetFilterRow({ field, isStage, }) {
718
874
  const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
719
875
  return (_jsx(ColumnFilterControl, { variant: "row", align: "end", icon: filterTypeIcon(field.config.filterType, isStage), label: field.label, valueSummary: summary, filterKey: field.config.filterKey, filterType: field.config.filterType, filterOptions: field.config.options, filterLoading: field.config.loading, filterSearchEndpoint: field.config.searchEndpoint, selectedValues: field.config.selectedValues, onFilterChange: field.config.onFilterChange, loadOptions: field.config.loadOptions }));
720
876
  }
721
- function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, disabled, model, columns, automationsAvailable, automationRules, onAutomationCreate, onAutomationUpdate, onAutomationRemove, customStage, onEditStage, onDeleteStage, children, }) {
877
+ // A real stage lane: sortable (header drag reorders) AND a card drop target.
878
+ // One `useSortable` provides both roles; `disabled.draggable` follows whether
879
+ // lane reordering is available, `disabled.droppable` follows the per-card
880
+ // transition gate.
881
+ function SortableStageLane({ reorderEnabled, droppableDisabled, laneProps, }) {
882
+ const { setNodeRef, setActivatorNodeRef, attributes, listeners, transform, transition, isDragging, isOver, } = useSortable({
883
+ id: laneProps.stage.key,
884
+ data: { type: 'lane' },
885
+ disabled: { draggable: !reorderEnabled, droppable: droppableDisabled },
886
+ });
887
+ const dnd = {
888
+ setNodeRef,
889
+ isOver,
890
+ isDragging,
891
+ draggable: reorderEnabled,
892
+ handleRef: setActivatorNodeRef,
893
+ handleProps: { ...attributes, ...listeners },
894
+ style: {
895
+ transform: CSS.Translate.toString(transform),
896
+ transition,
897
+ zIndex: isDragging ? 30 : undefined,
898
+ position: isDragging ? 'relative' : undefined,
899
+ },
900
+ };
901
+ return _jsx(KanbanLane, { ...laneProps, dnd: dnd });
902
+ }
903
+ // The synthetic "Sin etapa" lane: a card drop target, never draggable.
904
+ function DroppableStageLane({ droppableDisabled, laneProps, }) {
905
+ const { setNodeRef, isOver } = useDroppable({
906
+ id: laneProps.stage.key,
907
+ disabled: droppableDisabled,
908
+ });
909
+ return (_jsx(KanbanLane, { ...laneProps, dnd: { setNodeRef, isOver, draggable: false } }));
910
+ }
911
+ // A smart lane: sortable (header drag reorders) but NOT a card drop target — its
912
+ // droppable is disabled while a card is dragged, enabled while a lane is dragged
913
+ // (so a stage/smart lane can be reordered relative to it).
914
+ function SortableSmartLane({ droppableDisabled, smartProps, }) {
915
+ const { setNodeRef, setActivatorNodeRef, attributes, listeners, transform, transition, isDragging, } = useSortable({
916
+ id: smartProps.stage.key,
917
+ data: { type: 'lane' },
918
+ disabled: { draggable: false, droppable: droppableDisabled },
919
+ });
920
+ const dnd = {
921
+ setNodeRef,
922
+ isOver: false,
923
+ isDragging,
924
+ draggable: true,
925
+ handleRef: setActivatorNodeRef,
926
+ handleProps: { ...attributes, ...listeners },
927
+ style: {
928
+ transform: CSS.Translate.toString(transform),
929
+ transition,
930
+ zIndex: isDragging ? 30 : undefined,
931
+ position: isDragging ? 'relative' : undefined,
932
+ },
933
+ };
934
+ return _jsx(SmartLane, { ...smartProps, dnd: dnd });
935
+ }
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, }) {
722
937
  const { t } = useTranslation();
723
- const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
724
938
  // Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
725
939
  // container; a load in flight or an exhausted stage disables it.
726
940
  const { rootRef, sentinelRef } = useInfiniteScrollSentinel({
@@ -758,16 +972,17 @@ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMor
758
972
  text: laneFilter.text,
759
973
  }
760
974
  : undefined;
761
- return (_jsxs("div", { ref: setNodeRef,
975
+ return (_jsxs("div", { ref: dnd.setNodeRef,
762
976
  // Fluid width: lanes grow (flex-1) to fill the board when they all
763
977
  // fit, capped at max-w so a couple of lanes don't stretch absurdly
764
978
  // wide. Below min-w the board's overflow-x-auto takes over and the
765
979
  // lanes scroll horizontally at their minimum width.
766
980
  className: "group/lane flex min-w-[280px] max-w-[420px] flex-1 shrink-0 flex-col rounded-xl border bg-muted/30 transition-opacity", style: {
767
- opacity: dimmed ? 0.45 : 1,
768
- outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
981
+ opacity: dnd.isDragging ? 0.6 : dimmed ? 0.45 : 1,
982
+ outline: dnd.isOver ? '2px solid var(--ring, #3b82f6)' : 'none',
769
983
  outlineOffset: 2,
770
- }, "data-stage": stage.key, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_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', {
984
+ ...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', {
771
986
  defaultValue: 'Buscar en la columna',
772
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) => {
773
988
  if (e.key === 'Escape') {
@@ -844,6 +1059,7 @@ function LaneFilterButton({ fields, value, onChange, }) {
844
1059
  function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, draggable = true, }) {
845
1060
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
846
1061
  id: String(card.id),
1062
+ data: { type: 'card' },
847
1063
  disabled: !draggable,
848
1064
  });
849
1065
  const visibleActions = actions.filter((a) => isRowActionVisible(a, card));
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ 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
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';
7
+ export { useStageLayout, type StageLayout, type UseStageLayoutResult, } from './stage-layout';
7
8
  export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
8
9
  export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
9
10
  export * from './dynamic-form';