@asteby/metacore-runtime-react 23.5.1 → 23.7.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.
@@ -8,6 +8,8 @@ import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, Dr
8
8
  import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
9
9
  import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
10
10
  import { useApi } from './api-context';
11
+ import { useStageAutomations, StageAutomationsButton, } from './stage-automations';
12
+ import { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, AddStageColumn, CustomStageLaneMenu, CustomStageDialog, CustomStageDeleteDialog, SmartLane, } from './custom-stages';
11
13
  import { useDynamicFilters } from './use-dynamic-filters';
12
14
  import { FilterChipsRow, summarizeFilterValues, translateOptionLabels, } from './filter-chips';
13
15
  import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
@@ -236,6 +238,28 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
236
238
  const { t, i18n } = useTranslation();
237
239
  const api = useApi();
238
240
  const isDark = useIsDarkTheme();
241
+ // Stage automations (Bitrix-style per-lane rules). Degrades to no-op when
242
+ // the host has no `/stage-automations` endpoint — the ⚡ affordance hides.
243
+ const automations = useStageAutomations(model);
244
+ // Custom stages (Bitrix-style user-defined columns). Degrades to no-op when
245
+ // the host has no `/custom-stages` endpoint — the "+ Agregar etapa" column
246
+ // and lane menus simply don't render.
247
+ const customStages = useCustomStages(model);
248
+ // Dialog state: create/edit a stage, and the delete confirmation.
249
+ const [stageDialogOpen, setStageDialogOpen] = useState(false);
250
+ const [editingStage, setEditingStage] = useState(null);
251
+ const [deletingStage, setDeletingStage] = useState(null);
252
+ const openCreateStage = useCallback(() => {
253
+ setEditingStage(null);
254
+ setStageDialogOpen(true);
255
+ }, []);
256
+ const openEditStage = useCallback((s) => {
257
+ setEditingStage(s);
258
+ setStageDialogOpen(true);
259
+ }, []);
260
+ const openDeleteStage = useCallback((s) => {
261
+ setDeletingStage(s);
262
+ }, []);
239
263
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
240
264
  const cachedMeta = getMetadata(model);
241
265
  const [metadata, setMetadata] = useState(cachedMeta || null);
@@ -499,7 +523,23 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
499
523
  return next;
500
524
  });
501
525
  }, []);
502
- const stages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
526
+ const declaredStages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
527
+ // The kernel merges custom real stages into `metadata.stages` (custom: true)
528
+ // and serves smart lanes in `metadata.smart_lanes` — the metadata is the
529
+ // painting source (ops #704). The CRUD list (`customStages.stages`) only
530
+ // backs the management dialog: it carries the `id`/filters the metadata
531
+ // omits, so we still split it to build `customByKey` (and to fall back when
532
+ // the metadata hasn't caught up yet).
533
+ const { laneStages: customLaneStages, smartStages: crudSmartStages } = useMemo(() => splitCustomStages(customStages.stages), [customStages.stages]);
534
+ const { lanes: stages, customByKey } = useMemo(() => mergeLaneStages(declaredStages, customLaneStages), [declaredStages, customLaneStages]);
535
+ // Smart lanes to paint: metadata first, folding in CRUD ids by key.
536
+ const smartStages = useMemo(() => resolveSmartLanes(metadata?.smart_lanes, crudSmartStages, model), [metadata?.smart_lanes, crudSmartStages, model]);
537
+ // Position a newly created stage after every existing lane (real + smart).
538
+ const nextStagePosition = useMemo(() => {
539
+ const positions = customStages.stages.map((s) => s.position ?? 0);
540
+ const base = stages.length + smartStages.length;
541
+ return Math.max(base, ...positions, 0) + 1;
542
+ }, [customStages.stages, stages.length, smartStages.length]);
503
543
  const transitions = metadata?.transitions;
504
544
  const grouped = useMemo(() => groupByStage(records, groupByKey, stages), [records, groupByKey, stages]);
505
545
  const { title: titleCol, fields: fieldCols } = useMemo(() => (metadata ? selectCardColumns(metadata) : { title: null, fields: [] }), [metadata]);
@@ -615,33 +655,41 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
615
655
  })
616
656
  : t('kanban.noActiveFilters', {
617
657
  defaultValue: 'Sin filtros',
618
- }) }), _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: [_jsx("div", { className: "flex w-full min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
619
- const allCards = grouped.get(stage.key) ?? [];
620
- // Per-lane client-side narrowing (instant, scoped to this
621
- // stage). The funnel (field/value) and the lane search
622
- // (query) are AND-combined.
623
- const laneFilter = laneFilters[stage.key];
624
- let cards = allCards;
625
- if (laneFilter?.field) {
626
- cards = cards.filter((c) => cardMatchesLaneFunnel(c, laneFilter));
627
- }
628
- if (laneFilter?.query?.trim()) {
629
- cards = cards.filter((c) => cardMatchesLaneQuery(c, searchCols, laneFilter.query));
630
- }
631
- const droppableAllowed = !activeId ||
632
- stage.key === activeStage ||
633
- isTransitionAllowed(transitions, activeStage, stage.key);
634
- // Infinite scroll is per declared stage; the synthetic
635
- // "unassigned" lane can't be stage-scoped, so it never tops up.
636
- const laneState = lanePagination[stage.key];
637
- const isUnassigned = stage.key === UNASSIGNED_LANE;
638
- const laneHasMore = !isUnassigned && !laneState?.done;
639
- 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, {
640
- field: f?.field,
641
- values: f?.values,
642
- text: f?.text,
643
- }), onQueryChange: (q) => updateLaneFilter(stage.key, { query: q }), isDark: isDark, dimmed: !!activeId && !droppableAllowed, disabled: !!activeId && !droppableAllowed, 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));
644
- }) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] })] }));
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) => {
687
+ if (!o)
688
+ setDeletingStage(null);
689
+ }, stage: deletingStage, reassignTargets: stages.map((s) => ({
690
+ key: s.key,
691
+ label: s.label,
692
+ })), onConfirm: (s, reassignTo) => customStages.remove(s.id, reassignTo) })] }))] }));
645
693
  }
646
694
  /**
647
695
  * A per-data-type glyph for the Filtros panel rows (and their popover header):
@@ -670,7 +718,7 @@ function SheetFilterRow({ field, isStage, }) {
670
718
  const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
671
719
  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 }));
672
720
  }
673
- function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, disabled, children, }) {
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, }) {
674
722
  const { t } = useTranslation();
675
723
  const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
676
724
  // Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
@@ -721,7 +769,7 @@ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMor
721
769
  outlineOffset: 2,
722
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', {
723
771
  defaultValue: 'Buscar en la columna',
724
- }), 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 })] })] }), 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) => {
772
+ }), 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) => {
725
773
  if (e.key === 'Escape') {
726
774
  onQueryChange('');
727
775
  setSearchOpen(false);
@@ -793,12 +841,13 @@ function LaneFilterButton({ fields, value, onChange, }) {
793
841
  defaultValue: 'Contiene...',
794
842
  }), className: "h-8 w-full text-xs" })), _jsxs("div", { className: "flex gap-1.5", children: [_jsx(Button, { variant: "outline", size: "sm", className: "h-7 flex-1 text-xs", onClick: clear, disabled: !active && values.length === 0 && !text.trim(), children: t('kanban.clearFilters', { defaultValue: 'Limpiar' }) }), _jsxs(Button, { size: "sm", className: "h-7 flex-1 text-xs", onClick: apply, disabled: values.length === 0 && !text.trim(), children: [t('kanban.apply', { defaultValue: 'Aplicar' }), values.length > 0 ? ` (${values.length})` : ''] })] })] })] }));
795
843
  }
796
- function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
844
+ function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, draggable = true, }) {
797
845
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
798
846
  id: String(card.id),
847
+ disabled: !draggable,
799
848
  });
800
849
  const visibleActions = actions.filter((a) => isRowActionVisible(a, card));
801
- return (_jsx(Card, { ref: setNodeRef, ...attributes, ...listeners, className: "w-full min-w-0 cursor-grab active:cursor-grabbing border-border/70 shadow-sm", style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), visibleActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0 -mr-1 -mt-1",
850
+ return (_jsx(Card, { ref: draggable ? setNodeRef : undefined, ...(draggable ? attributes : {}), ...(draggable ? listeners : {}), className: `w-full min-w-0 border-border/70 shadow-sm ${draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`, style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsx("span", { className: "truncate", children: String(card.id) })) }), visibleActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0 -mr-1 -mt-1",
802
851
  // Don't start a drag / card click from the menu button.
803
852
  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) => {
804
853
  e.stopPropagation();
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function FieldGrid({ children, className }: {
3
+ children: ReactNode;
4
+ className?: string;
5
+ }): import("react").JSX.Element;
6
+ export declare function FieldCell({ children, fullWidth, className, }: {
7
+ children: ReactNode;
8
+ /** Span both columns — textareas, line-items grids, embedded relations. */
9
+ fullWidth?: boolean;
10
+ className?: string;
11
+ }): import("react").JSX.Element;
12
+ export declare function FieldLabel({ htmlFor, required, children, }: {
13
+ htmlFor?: string;
14
+ required?: boolean;
15
+ children: ReactNode;
16
+ }): import("react").JSX.Element;
17
+ //# sourceMappingURL=field-grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-grid.d.ts","sourceRoot":"","sources":["../src/field-grid.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAM7F;AAED,wBAAgB,SAAS,CAAC,EACtB,QAAQ,EACR,SAAS,EACT,SAAS,GACZ,EAAE;IACC,QAAQ,EAAE,SAAS,CAAA;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,+BAMA;AAED,wBAAgB,UAAU,CAAC,EACvB,OAAO,EACP,QAAQ,EACR,QAAQ,GACX,EAAE;IACC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;CACtB,+BAOA"}
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Label } from '@asteby/metacore-ui/primitives';
3
+ import { cn } from '@asteby/metacore-ui/lib';
4
+ export function FieldGrid({ children, className }) {
5
+ return (_jsx("div", { className: cn('grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2', className), children: children }));
6
+ }
7
+ export function FieldCell({ children, fullWidth, className, }) {
8
+ return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-1.5', fullWidth && 'sm:col-span-2', className), children: children }));
9
+ }
10
+ export function FieldLabel({ htmlFor, required, children, }) {
11
+ return (_jsxs(Label, { htmlFor: htmlFor, className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [children, required && _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }));
12
+ }
package/dist/index.d.ts CHANGED
@@ -2,9 +2,12 @@ export * from './types';
2
2
  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
+ 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';
5
7
  export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
6
8
  export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
7
9
  export * from './dynamic-form';
10
+ export { FieldGrid, FieldCell, FieldLabel } from './field-grid';
8
11
  export { ActionModalDispatcher, type ActionModalProps, } from './action-modal-dispatcher';
9
12
  export { ModelActionToolbar, useModelActions, type ModelActionToolbarProps, type ActionPlacement, } from './model-action-toolbar';
10
13
  export * from './addon-loader';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,EACvB,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,GACnC,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC7B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACH,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,gBAAgB,GACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACH,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC/B,MAAM,uBAAuB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC/D,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,kBAAkB,EAClB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACvB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACH,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,OAAO,EACP,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,wBAAwB,GAChC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,oBAAoB,EACpB,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,OAAO,EACZ,KAAK,SAAS,GACjB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EACH,OAAO,EACP,QAAQ,EACR,cAAc,EACd,UAAU,EACV,QAAQ,EACR,WAAW,EACX,WAAW,EACX,UAAU,EACV,SAAS,EACT,UAAU,EACV,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,cAAc,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EACH,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,KAAK,mBAAmB,EACxB,KAAK,SAAS,IAAI,uBAAuB,GAC5C,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzE,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,0BAA0B,GAClC,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,GAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,gBAAgB,EAChB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,aAAa,EACb,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACR,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACH,UAAU,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,EACd,KAAK,iBAAiB,GACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,cAAc,EACd,cAAc,EACd,SAAS,EACT,UAAU,EACV,KAAK,mBAAmB,GAC3B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,UAAU,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -7,9 +7,12 @@ export * from './types';
7
7
  export * from './options-context';
8
8
  export * from './dynamic-table';
9
9
  export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
10
+ export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, } from './stage-automations';
11
+ export { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, smartLaneParams, customStageFilterFields, isCustomStageDraftValid, slugifyStageKey, emptyCustomStageFilter, AddStageColumn, CustomStageLaneMenu, CustomStageDialog, CustomStageDeleteDialog, SmartLane, CUSTOM_STAGE_COLORS, CUSTOM_STAGE_FILTER_OPS, } from './custom-stages';
10
12
  export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, } from './dynamic-view';
11
13
  export { useDynamicFilters, } from './use-dynamic-filters';
12
14
  export * from './dynamic-form';
15
+ export { FieldGrid, FieldCell, FieldLabel } from './field-grid';
13
16
  export { ActionModalDispatcher, } from './action-modal-dispatcher';
14
17
  export { ModelActionToolbar, useModelActions, } from './model-action-toolbar';
15
18
  export * from './addon-loader';
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import type { ApiClient } from './api-context';
3
+ import type { ColumnDefinition } from './types';
4
+ export type StageAutomationActionType = 'add_tag' | 'remove_tag' | 'set_field';
5
+ export interface StageAutomationAction {
6
+ type: StageAutomationActionType;
7
+ field: string;
8
+ value: string;
9
+ }
10
+ export interface StageAutomation {
11
+ id: string | number;
12
+ model: string;
13
+ /** Source stage the card is leaving. `'*'` = any stage. */
14
+ from_stage: string;
15
+ /** Destination stage that triggers the rule (the lane's stage key). */
16
+ to_stage: string;
17
+ actions: StageAutomationAction[];
18
+ enabled: boolean;
19
+ }
20
+ /** Draft for a new rule — a single action, keyed to a destination stage. */
21
+ export interface NewStageAutomation {
22
+ model: string;
23
+ from_stage: string;
24
+ to_stage: string;
25
+ actions: StageAutomationAction[];
26
+ enabled: boolean;
27
+ }
28
+ /** Whether a column holds a tag/json array (valid target for add/remove_tag). */
29
+ export declare function isTagColumn(col: ColumnDefinition): boolean;
30
+ /**
31
+ * Columns a given action type may target:
32
+ * - add_tag / remove_tag → only tag/json columns
33
+ * - set_field → any editable (non-readonly, non-hidden) column
34
+ * Hidden and readonly columns are never offered.
35
+ */
36
+ export declare function automationFieldOptions(columns: ColumnDefinition[], actionType: StageAutomationActionType): ColumnDefinition[];
37
+ /** Rules grouped by their destination stage key (only what the lane needs). */
38
+ export declare function groupAutomationsByStage(rules: StageAutomation[]): Map<string, StageAutomation[]>;
39
+ /** Count of ENABLED rules for a stage — drives the lane's ⚡ indicator. */
40
+ export declare function activeAutomationCount(rules: StageAutomation[] | undefined): number;
41
+ export interface UseStageAutomationsResult {
42
+ /** False when the endpoint is missing/errored — hide the ⚡ affordance. */
43
+ available: boolean;
44
+ loading: boolean;
45
+ /** Destination-stage-keyed rules. */
46
+ byStage: Map<string, StageAutomation[]>;
47
+ create: (draft: NewStageAutomation) => Promise<void>;
48
+ update: (id: StageAutomation['id'], patch: Partial<StageAutomation>) => Promise<void>;
49
+ remove: (id: StageAutomation['id']) => Promise<void>;
50
+ }
51
+ /**
52
+ * Loads a model's stage automations and exposes CRUD. Swallows a missing
53
+ * endpoint (404 / network error) into `available: false` so the kanban never
54
+ * breaks; real mutation failures surface a toast and re-throw so the dialog can
55
+ * keep its draft.
56
+ */
57
+ export declare function useStageAutomations(model: string): UseStageAutomationsResult;
58
+ export interface StageAutomationsButtonProps {
59
+ model: string;
60
+ /** The lane's stage key + human label (destination that triggers rules). */
61
+ stageKey: string;
62
+ stageLabel: string;
63
+ columns: ColumnDefinition[];
64
+ rules: StageAutomation[];
65
+ onCreate: (draft: NewStageAutomation) => Promise<void>;
66
+ onUpdate: (id: StageAutomation['id'], patch: Partial<StageAutomation>) => Promise<void>;
67
+ onRemove: (id: StageAutomation['id']) => Promise<void>;
68
+ }
69
+ /**
70
+ * The per-lane ⚡ button: shows an active-rule count badge and opens the rule
71
+ * editor for THIS stage. Rendered only when the endpoint is available.
72
+ */
73
+ export declare function StageAutomationsButton({ model, stageKey, stageLabel, columns, rules, onCreate, onUpdate, onRemove, }: StageAutomationsButtonProps): React.JSX.Element;
74
+ export type { ApiClient };
75
+ //# sourceMappingURL=stage-automations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-automations.d.ts","sourceRoot":"","sources":["../src/stage-automations.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,264 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Stage automations — a Bitrix-style "when a card enters this stage, run these
3
+ // actions" rule editor, surfaced per-lane in DynamicKanban. The feature is
4
+ // generic over any model: rules live server-side (the host wires the
5
+ // `/stage-automations` REST endpoints against the same api client the rest of
6
+ // the dynamic runtime uses) and reference the model's own columns.
7
+ //
8
+ // Non-intrusive by design: if the endpoint 404s or errors, the hook reports
9
+ // `available: false` and the lane simply omits the ⚡ affordance — the board
10
+ // keeps working. All UI text goes through t() with a Spanish defaultValue so a
11
+ // host that ships no `dynamic.automations.*` keys still renders in Spanish.
12
+ import { useCallback, useEffect, useMemo, useState } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { Zap, Trash2, Plus } from 'lucide-react';
15
+ import { toast } from 'sonner';
16
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
17
+ import { useApi } from './api-context';
18
+ // ---------------------------------------------------------------------------
19
+ // Pure helpers (exported for unit tests — no React, no transport)
20
+ // ---------------------------------------------------------------------------
21
+ const ACTION_TYPES = [
22
+ 'add_tag',
23
+ 'remove_tag',
24
+ 'set_field',
25
+ ];
26
+ /** Whether a column holds a tag/json array (valid target for add/remove_tag). */
27
+ export function isTagColumn(col) {
28
+ const t = col.type;
29
+ const style = col.cellStyle;
30
+ return t === 'tags' || t === 'json' || style === 'tags';
31
+ }
32
+ /**
33
+ * Columns a given action type may target:
34
+ * - add_tag / remove_tag → only tag/json columns
35
+ * - set_field → any editable (non-readonly, non-hidden) column
36
+ * Hidden and readonly columns are never offered.
37
+ */
38
+ export function automationFieldOptions(columns, actionType) {
39
+ return columns.filter((c) => {
40
+ if (c.hidden)
41
+ return false;
42
+ const ro = !!c.readonly ||
43
+ !!c.read_only;
44
+ if (ro)
45
+ return false;
46
+ if (actionType === 'add_tag' || actionType === 'remove_tag') {
47
+ return isTagColumn(c);
48
+ }
49
+ return true;
50
+ });
51
+ }
52
+ /** Rules grouped by their destination stage key (only what the lane needs). */
53
+ export function groupAutomationsByStage(rules) {
54
+ const map = new Map();
55
+ for (const r of rules) {
56
+ const arr = map.get(r.to_stage) ?? [];
57
+ arr.push(r);
58
+ map.set(r.to_stage, arr);
59
+ }
60
+ return map;
61
+ }
62
+ /** Count of ENABLED rules for a stage — drives the lane's ⚡ indicator. */
63
+ export function activeAutomationCount(rules) {
64
+ if (!rules)
65
+ return 0;
66
+ return rules.filter((r) => r.enabled).length;
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Data hook
70
+ // ---------------------------------------------------------------------------
71
+ function unwrap(res) {
72
+ const body = res?.data;
73
+ // Envelope {success, data}; tolerate a bare array/object too.
74
+ if (body && typeof body === 'object' && 'data' in body)
75
+ return body.data;
76
+ return body;
77
+ }
78
+ /**
79
+ * Loads a model's stage automations and exposes CRUD. Swallows a missing
80
+ * endpoint (404 / network error) into `available: false` so the kanban never
81
+ * breaks; real mutation failures surface a toast and re-throw so the dialog can
82
+ * keep its draft.
83
+ */
84
+ export function useStageAutomations(model) {
85
+ const api = useApi();
86
+ const { t } = useTranslation();
87
+ const [available, setAvailable] = useState(true);
88
+ const [loading, setLoading] = useState(true);
89
+ const [rules, setRules] = useState([]);
90
+ const load = useCallback(async () => {
91
+ setLoading(true);
92
+ try {
93
+ const res = await api.get(`/stage-automations?model=${encodeURIComponent(model)}`);
94
+ const data = unwrap(res);
95
+ setRules(Array.isArray(data) ? data : []);
96
+ setAvailable(true);
97
+ }
98
+ catch {
99
+ // Endpoint absent or errored — degrade silently.
100
+ setRules([]);
101
+ setAvailable(false);
102
+ }
103
+ finally {
104
+ setLoading(false);
105
+ }
106
+ }, [api, model]);
107
+ useEffect(() => {
108
+ void load();
109
+ }, [load]);
110
+ const create = useCallback(async (draft) => {
111
+ try {
112
+ const res = await api.post('/stage-automations', draft);
113
+ const created = unwrap(res);
114
+ await load();
115
+ if (!created)
116
+ return;
117
+ }
118
+ catch (e) {
119
+ toast.error(t('dynamic.automations.saveError', {
120
+ defaultValue: 'No se pudo guardar la automatización',
121
+ }));
122
+ throw e;
123
+ }
124
+ }, [api, load, t]);
125
+ const update = useCallback(async (id, patch) => {
126
+ try {
127
+ await api.put(`/stage-automations/${id}`, patch);
128
+ await load();
129
+ }
130
+ catch (e) {
131
+ toast.error(t('dynamic.automations.saveError', {
132
+ defaultValue: 'No se pudo guardar la automatización',
133
+ }));
134
+ throw e;
135
+ }
136
+ }, [api, load, t]);
137
+ const remove = useCallback(async (id) => {
138
+ try {
139
+ await api.delete(`/stage-automations/${id}`);
140
+ await load();
141
+ }
142
+ catch (e) {
143
+ toast.error(t('dynamic.automations.deleteError', {
144
+ defaultValue: 'No se pudo eliminar la automatización',
145
+ }));
146
+ throw e;
147
+ }
148
+ }, [api, load, t]);
149
+ const byStage = useMemo(() => groupAutomationsByStage(rules), [rules]);
150
+ return { available, loading, byStage, create, update, remove };
151
+ }
152
+ /**
153
+ * The per-lane ⚡ button: shows an active-rule count badge and opens the rule
154
+ * editor for THIS stage. Rendered only when the endpoint is available.
155
+ */
156
+ export function StageAutomationsButton({ model, stageKey, stageLabel, columns, rules, onCreate, onUpdate, onRemove, }) {
157
+ const { t } = useTranslation();
158
+ const [open, setOpen] = useState(false);
159
+ const activeCount = activeAutomationCount(rules);
160
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => setOpen(true), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${activeCount > 0 ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('dynamic.automations.open', {
161
+ defaultValue: 'Automatizaciones de la etapa',
162
+ }), "data-testid": `automations-trigger-${stageKey}`, children: [_jsx(Zap, { className: "h-3.5 w-3.5" }), activeCount > 0 && (_jsx("span", { className: "absolute -right-1 -top-1 flex min-w-3.5 items-center justify-center rounded-full bg-primary px-1 text-[9px] font-semibold leading-none text-primary-foreground", children: activeCount }))] }), open && (_jsx(StageAutomationsDialog, { open: open, onOpenChange: setOpen, model: model, stageKey: stageKey, stageLabel: stageLabel, columns: columns, rules: rules, onCreate: onCreate, onUpdate: onUpdate, onRemove: onRemove }))] }));
163
+ }
164
+ function describeAction(action, columns, t) {
165
+ const col = columns.find((c) => c.key === action.field);
166
+ const fieldLabel = col ? t(col.label, { defaultValue: col.label }) : action.field;
167
+ switch (action.type) {
168
+ case 'add_tag':
169
+ return t('dynamic.automations.summary.addTag', {
170
+ defaultValue: 'Agregar tag "{{value}}" a {{field}}',
171
+ value: action.value,
172
+ field: fieldLabel,
173
+ });
174
+ case 'remove_tag':
175
+ return t('dynamic.automations.summary.removeTag', {
176
+ defaultValue: 'Quitar tag "{{value}}" de {{field}}',
177
+ value: action.value,
178
+ field: fieldLabel,
179
+ });
180
+ default:
181
+ return t('dynamic.automations.summary.setField', {
182
+ defaultValue: 'Setear {{field}} = "{{value}}"',
183
+ value: action.value,
184
+ field: fieldLabel,
185
+ });
186
+ }
187
+ }
188
+ function StageAutomationsDialog({ open, onOpenChange, model, stageKey, stageLabel, columns, rules, onCreate, onUpdate, onRemove, }) {
189
+ const { t } = useTranslation();
190
+ const [actionType, setActionType] = useState('add_tag');
191
+ const [field, setField] = useState('');
192
+ const [value, setValue] = useState('');
193
+ const [saving, setSaving] = useState(false);
194
+ const fieldChoices = useMemo(() => automationFieldOptions(columns, actionType), [columns, actionType]);
195
+ // Keep the selected field valid for the current action type.
196
+ useEffect(() => {
197
+ if (!fieldChoices.some((c) => c.key === field)) {
198
+ setField(fieldChoices[0]?.key ?? '');
199
+ }
200
+ }, [fieldChoices, field]);
201
+ const label = t(stageLabel, { defaultValue: stageLabel });
202
+ const canSubmit = !!field && !!value.trim() && !saving;
203
+ const submit = async () => {
204
+ if (!canSubmit)
205
+ return;
206
+ setSaving(true);
207
+ try {
208
+ await onCreate({
209
+ model,
210
+ from_stage: '*',
211
+ to_stage: stageKey,
212
+ actions: [{ type: actionType, field, value: value.trim() }],
213
+ enabled: true,
214
+ });
215
+ setValue('');
216
+ toast.success(t('dynamic.automations.saved', {
217
+ defaultValue: 'Automatización guardada',
218
+ }));
219
+ }
220
+ catch {
221
+ // toast already surfaced by the hook; keep the draft.
222
+ }
223
+ finally {
224
+ setSaving(false);
225
+ }
226
+ };
227
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('dynamic.automations.title', {
228
+ defaultValue: 'Automatizaciones · {{stage}}',
229
+ stage: label,
230
+ }) }), _jsx(DialogDescription, { children: t('dynamic.automations.description', {
231
+ defaultValue: 'Al entrar una tarjeta a esta etapa se ejecutan estas acciones.',
232
+ }) })] }), _jsx("div", { className: "flex flex-col gap-2", children: rules.length === 0 ? (_jsx("p", { className: "rounded-md border border-dashed px-3 py-6 text-center text-sm text-muted-foreground", children: t('dynamic.automations.empty', {
233
+ defaultValue: 'Sin automatizaciones para esta etapa.',
234
+ }) })) : (rules.map((rule) => (_jsxs("div", { className: "flex items-center gap-2 rounded-md border px-3 py-2", "data-testid": `automation-rule-${rule.id}`, children: [_jsx("div", { className: "min-w-0 flex-1", children: rule.actions.map((a, i) => (_jsx("p", { className: `truncate text-sm ${rule.enabled ? '' : 'text-muted-foreground line-through'}`, children: describeAction(a, columns, t) }, i))) }), _jsx(Switch, { checked: rule.enabled, onCheckedChange: (checked) => void onUpdate(rule.id, { enabled: checked }), "aria-label": t('dynamic.automations.toggle', {
235
+ defaultValue: 'Activar automatización',
236
+ }), "data-testid": `automation-toggle-${rule.id}` }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 text-muted-foreground hover:text-destructive", onClick: () => {
237
+ void onRemove(rule.id).then(() => toast.success(t('dynamic.automations.deleted', {
238
+ defaultValue: 'Automatización eliminada',
239
+ })));
240
+ }, "aria-label": t('dynamic.automations.delete', {
241
+ defaultValue: 'Eliminar automatización',
242
+ }), "data-testid": `automation-delete-${rule.id}`, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, String(rule.id))))) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 rounded-md border bg-muted/30 p-3 sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.automations.actionLabel', {
243
+ defaultValue: 'Acción',
244
+ }) }), _jsxs(Select, { value: actionType, onValueChange: (v) => setActionType(v), children: [_jsx(SelectTrigger, { "data-testid": "automation-action-select", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ACTION_TYPES.map((at) => (_jsx(SelectItem, { value: at, children: t(`dynamic.automations.action.${at}`, {
245
+ defaultValue: at === 'add_tag'
246
+ ? 'Agregar tag'
247
+ : at === 'remove_tag'
248
+ ? 'Quitar tag'
249
+ : 'Setear campo',
250
+ }) }, at))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.automations.fieldLabel', {
251
+ defaultValue: 'Campo',
252
+ }) }), _jsxs(Select, { value: field, onValueChange: setField, disabled: fieldChoices.length === 0, children: [_jsx(SelectTrigger, { "data-testid": "automation-field-select", children: _jsx(SelectValue, { placeholder: t('dynamic.automations.noFields', {
253
+ defaultValue: 'Sin campos',
254
+ }) }) }), _jsx(SelectContent, { children: fieldChoices.map((c) => (_jsx(SelectItem, { value: c.key, children: t(c.label, { defaultValue: c.label }) }, c.key))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.automations.valueLabel', {
255
+ defaultValue: 'Valor',
256
+ }) }), _jsx(Input, { value: value, onChange: (e) => setValue(e.target.value), onKeyDown: (e) => {
257
+ if (e.key === 'Enter')
258
+ void submit();
259
+ }, placeholder: t('dynamic.automations.valuePlaceholder', {
260
+ defaultValue: 'Valor...',
261
+ }), "data-testid": "automation-value-input" })] })] }), _jsx(DialogFooter, { children: _jsxs(Button, { onClick: () => void submit(), disabled: !canSubmit, className: "gap-1", "data-testid": "automation-add", children: [_jsx(Plus, { className: "h-4 w-4" }), t('dynamic.automations.add', {
262
+ defaultValue: 'Agregar regla',
263
+ })] }) })] }) }));
264
+ }