@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.
- package/CHANGELOG.md +16 -0
- package/dist/action-modal-dispatcher.js +8 -7
- package/dist/custom-stages.d.ts +171 -0
- package/dist/custom-stages.d.ts.map +1 -0
- package/dist/custom-stages.js +535 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +7 -4
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +81 -32
- package/dist/field-grid.d.ts +17 -0
- package/dist/field-grid.d.ts.map +1 -0
- package/dist/field-grid.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/stage-automations.d.ts +75 -0
- package/dist/stage-automations.d.ts.map +1 -0
- package/dist/stage-automations.js +264 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/action-modal-grid-layout.test.tsx +86 -0
- package/src/__tests__/custom-stages.test.tsx +480 -0
- package/src/__tests__/stage-automations.test.tsx +243 -0
- package/src/action-modal-dispatcher.tsx +29 -30
- package/src/custom-stages.tsx +1113 -0
- package/src/dialogs/dynamic-record.tsx +14 -8
- package/src/dynamic-kanban.tsx +203 -5
- package/src/field-grid.tsx +57 -0
- package/src/index.ts +39 -0
- package/src/stage-automations.tsx +580 -0
- package/src/types.ts +26 -0
package/dist/dynamic-kanban.js
CHANGED
|
@@ -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
|
|
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: [
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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:
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"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"}
|
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
|
+
}
|