@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
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// Custom stages — a Bitrix-style "add your own column" affordance for
|
|
3
|
+
// DynamicKanban, generic over any model. Two flavors:
|
|
4
|
+
//
|
|
5
|
+
// - type: "stage" → a REAL lane. The backend persists it and also surfaces
|
|
6
|
+
// it in the model's `metadata.stages`, so cards drag in/out of it exactly
|
|
7
|
+
// like a declared stage. We only add the create/edit/delete UI + merge any
|
|
8
|
+
// custom stage the metadata hasn't caught up to yet (tolerant).
|
|
9
|
+
//
|
|
10
|
+
// - type: "smart" → a VIRTUAL lane. Nothing is stored on the card; the lane
|
|
11
|
+
// is defined by a set of `filters` and is populated by querying the list
|
|
12
|
+
// with those filters (the same `f_<field>=value` params the board already
|
|
13
|
+
// speaks). Cards in a smart lane are read-only (no drag), and the header
|
|
14
|
+
// shows a funnel icon.
|
|
15
|
+
//
|
|
16
|
+
// Non-intrusive by design: the CRUD lives server-side behind `/custom-stages`
|
|
17
|
+
// (same api client as the rest of the dynamic runtime). If the endpoint 404s
|
|
18
|
+
// or errors the hook reports `available: false`, the "+ Agregar etapa" column
|
|
19
|
+
// and the lane context menus simply don't render, and the board keeps working.
|
|
20
|
+
// All UI text goes through t() with a Spanish defaultValue.
|
|
21
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
23
|
+
import { Plus, Trash2, Pencil, MoreVertical, Filter, X } from 'lucide-react';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
26
|
+
import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
|
|
27
|
+
import { useApi } from './api-context';
|
|
28
|
+
// ~8 preset palette names understood by `generateBadgeStyles`.
|
|
29
|
+
export const CUSTOM_STAGE_COLORS = [
|
|
30
|
+
'slate',
|
|
31
|
+
'blue',
|
|
32
|
+
'green',
|
|
33
|
+
'amber',
|
|
34
|
+
'red',
|
|
35
|
+
'purple',
|
|
36
|
+
'pink',
|
|
37
|
+
'cyan',
|
|
38
|
+
];
|
|
39
|
+
export const CUSTOM_STAGE_FILTER_OPS = [
|
|
40
|
+
'eq',
|
|
41
|
+
'neq',
|
|
42
|
+
'contains',
|
|
43
|
+
'in',
|
|
44
|
+
];
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Pure helpers (exported for unit tests — no React, no transport)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** A blank filter row for the smart-lane condition builder. */
|
|
49
|
+
export function emptyCustomStageFilter(field = '') {
|
|
50
|
+
return { field, op: 'eq', value: '' };
|
|
51
|
+
}
|
|
52
|
+
/** Enabled custom stages split by flavor (disabled ones are dropped). */
|
|
53
|
+
export function splitCustomStages(stages) {
|
|
54
|
+
const laneStages = [];
|
|
55
|
+
const smartStages = [];
|
|
56
|
+
for (const s of stages ?? []) {
|
|
57
|
+
if (s.enabled === false)
|
|
58
|
+
continue;
|
|
59
|
+
if (s.type === 'smart')
|
|
60
|
+
smartStages.push(s);
|
|
61
|
+
else
|
|
62
|
+
laneStages.push(s);
|
|
63
|
+
}
|
|
64
|
+
return { laneStages, smartStages };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Merges `type: "stage"` custom stages into the model's declared lanes. A
|
|
68
|
+
* custom stage whose key the metadata ALREADY carries (the backend surfaced it
|
|
69
|
+
* in `metadata.stages`) is not duplicated — it's just tagged as custom so the
|
|
70
|
+
* lane grows an edit/delete menu. Unknown-key custom stages are appended.
|
|
71
|
+
* Returns the merged lanes (sorted by order) plus a `key → CustomStage` map so
|
|
72
|
+
* the board can decorate the custom lanes.
|
|
73
|
+
*/
|
|
74
|
+
export function mergeLaneStages(declared, customLaneStages) {
|
|
75
|
+
const customByKey = new Map();
|
|
76
|
+
for (const cs of customLaneStages)
|
|
77
|
+
customByKey.set(cs.key, cs);
|
|
78
|
+
const present = new Set(declared.map((s) => s.key));
|
|
79
|
+
const lanes = [...declared];
|
|
80
|
+
for (const cs of customLaneStages) {
|
|
81
|
+
if (present.has(cs.key))
|
|
82
|
+
continue;
|
|
83
|
+
lanes.push({
|
|
84
|
+
key: cs.key,
|
|
85
|
+
label: cs.label,
|
|
86
|
+
color: cs.color,
|
|
87
|
+
order: cs.position,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
lanes.sort((a, b) => {
|
|
91
|
+
const ao = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
92
|
+
const bo = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
93
|
+
return ao - bo;
|
|
94
|
+
});
|
|
95
|
+
return { lanes, customByKey };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Serializes a smart lane's filters into the board's list query params. The ops
|
|
99
|
+
* list endpoint (ops #704) takes a SINGLE `f_<field>` param whose value carries
|
|
100
|
+
* the operator as a `OP:value` prefix:
|
|
101
|
+
* - eq → `f_<field>=EQ:value`
|
|
102
|
+
* - neq → `f_<field>=NEQ:value`
|
|
103
|
+
* - contains → `f_<field>=HAS:value` (membership in a jsonb array — NOT the
|
|
104
|
+
* text-substring ILIKE; that's why it's HAS, not CONTAINS)
|
|
105
|
+
* - in → `f_<field>=IN:v1,v2,...` (comma-separated after `IN:`)
|
|
106
|
+
* Empty fields/values are skipped.
|
|
107
|
+
*/
|
|
108
|
+
export function smartLaneParams(filters) {
|
|
109
|
+
const params = {};
|
|
110
|
+
for (const f of filters ?? []) {
|
|
111
|
+
if (!f.field || f.value == null || String(f.value).trim() === '')
|
|
112
|
+
continue;
|
|
113
|
+
const v = String(f.value).trim();
|
|
114
|
+
switch (f.op) {
|
|
115
|
+
case 'neq':
|
|
116
|
+
params[`f_${f.field}`] = `NEQ:${v}`;
|
|
117
|
+
break;
|
|
118
|
+
case 'contains':
|
|
119
|
+
params[`f_${f.field}`] = `HAS:${v}`;
|
|
120
|
+
break;
|
|
121
|
+
case 'in':
|
|
122
|
+
params[`f_${f.field}`] = `IN:${v}`;
|
|
123
|
+
break;
|
|
124
|
+
case 'eq':
|
|
125
|
+
default:
|
|
126
|
+
params[`f_${f.field}`] = `EQ:${v}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return params;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolves the smart lanes to paint. The kernel's `metadata.smart_lanes` is the
|
|
133
|
+
* source of truth for rendering (ops #704); the CRUD list only backs the
|
|
134
|
+
* management dialog. So when metadata carries smart lanes we map those, folding
|
|
135
|
+
* in the CRUD entry's `id` (matched by key) so the Editar menu can PUT/DELETE.
|
|
136
|
+
* When metadata omits them (older host / metadata lag) we tolerate the gap and
|
|
137
|
+
* fall back to the CRUD smart stages. Returns `CustomStage[]` either way.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveSmartLanes(metaSmartLanes, crudSmartStages, model) {
|
|
140
|
+
if (!metaSmartLanes?.length)
|
|
141
|
+
return crudSmartStages;
|
|
142
|
+
const crudByKey = new Map(crudSmartStages.map((s) => [s.key, s]));
|
|
143
|
+
return metaSmartLanes.map((lane, i) => {
|
|
144
|
+
const match = crudByKey.get(lane.key);
|
|
145
|
+
return {
|
|
146
|
+
id: match?.id ?? lane.key,
|
|
147
|
+
model,
|
|
148
|
+
key: lane.key,
|
|
149
|
+
label: lane.label,
|
|
150
|
+
color: lane.color ?? match?.color ?? 'slate',
|
|
151
|
+
position: lane.order ?? match?.position ?? i,
|
|
152
|
+
type: 'smart',
|
|
153
|
+
filters: (lane.filters ?? []).map((f) => ({
|
|
154
|
+
field: f.field,
|
|
155
|
+
op: f.op,
|
|
156
|
+
value: f.value,
|
|
157
|
+
})),
|
|
158
|
+
enabled: true,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Columns offered in the condition builder: visible, non-id model columns. */
|
|
163
|
+
export function customStageFilterFields(columns) {
|
|
164
|
+
return columns.filter((c) => !c.hidden && c.key !== 'id');
|
|
165
|
+
}
|
|
166
|
+
/** Whether a draft is complete enough to save. */
|
|
167
|
+
export function isCustomStageDraftValid(draft) {
|
|
168
|
+
if (!draft.label.trim())
|
|
169
|
+
return false;
|
|
170
|
+
if (draft.type === 'smart') {
|
|
171
|
+
return draft.filters.some((f) => f.field && String(f.value).trim() !== '');
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** Slugify a label into a stable-ish lane key (used only when creating). */
|
|
176
|
+
export function slugifyStageKey(label) {
|
|
177
|
+
return (label
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.normalize('NFD')
|
|
180
|
+
.replace(/[̀-ͯ]/g, '')
|
|
181
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
182
|
+
.replace(/^_+|_+$/g, '')
|
|
183
|
+
.slice(0, 48) || `stage_${Date.now().toString(36)}`);
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Data hook
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
function unwrap(res) {
|
|
189
|
+
const body = res?.data;
|
|
190
|
+
if (body && typeof body === 'object' && 'data' in body)
|
|
191
|
+
return body.data;
|
|
192
|
+
return body;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Loads a model's custom stages and exposes CRUD. A missing endpoint (404 /
|
|
196
|
+
* network error) degrades to `available: false` so the kanban never breaks;
|
|
197
|
+
* real mutation failures surface a toast and re-throw so the dialog keeps its
|
|
198
|
+
* draft. A 409 on delete (existing cards) is re-thrown WITHOUT a generic toast
|
|
199
|
+
* so the delete flow can show a targeted message.
|
|
200
|
+
*/
|
|
201
|
+
export function useCustomStages(model) {
|
|
202
|
+
const api = useApi();
|
|
203
|
+
const { t } = useTranslation();
|
|
204
|
+
const [available, setAvailable] = useState(true);
|
|
205
|
+
const [loading, setLoading] = useState(true);
|
|
206
|
+
const [stages, setStages] = useState([]);
|
|
207
|
+
const load = useCallback(async () => {
|
|
208
|
+
setLoading(true);
|
|
209
|
+
try {
|
|
210
|
+
const res = await api.get(`/custom-stages?model=${encodeURIComponent(model)}`);
|
|
211
|
+
const data = unwrap(res);
|
|
212
|
+
setStages(Array.isArray(data) ? data : []);
|
|
213
|
+
setAvailable(true);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
setStages([]);
|
|
217
|
+
setAvailable(false);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
setLoading(false);
|
|
221
|
+
}
|
|
222
|
+
}, [api, model]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
void load();
|
|
225
|
+
}, [load]);
|
|
226
|
+
const create = useCallback(async (draft) => {
|
|
227
|
+
try {
|
|
228
|
+
await api.post('/custom-stages', draft);
|
|
229
|
+
await load();
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
toast.error(t('dynamic.custom_stages.save_error', {
|
|
233
|
+
defaultValue: 'No se pudo guardar la etapa',
|
|
234
|
+
}));
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
}, [api, load, t]);
|
|
238
|
+
const update = useCallback(async (id, patch) => {
|
|
239
|
+
try {
|
|
240
|
+
await api.put(`/custom-stages/${id}`, patch);
|
|
241
|
+
await load();
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
toast.error(t('dynamic.custom_stages.save_error', {
|
|
245
|
+
defaultValue: 'No se pudo guardar la etapa',
|
|
246
|
+
}));
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
249
|
+
}, [api, load, t]);
|
|
250
|
+
const remove = useCallback(async (id, reassignTo) => {
|
|
251
|
+
try {
|
|
252
|
+
await api.delete(`/custom-stages/${id}`, reassignTo
|
|
253
|
+
? { params: { reassign_to: reassignTo } }
|
|
254
|
+
: undefined);
|
|
255
|
+
await load();
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
// 409 = the stage still holds cards; let the caller decide how to
|
|
259
|
+
// surface it (reassign prompt) instead of a generic error toast.
|
|
260
|
+
if (e?.response?.status === 409)
|
|
261
|
+
throw e;
|
|
262
|
+
toast.error(t('dynamic.custom_stages.delete_error', {
|
|
263
|
+
defaultValue: 'No se pudo eliminar la etapa',
|
|
264
|
+
}));
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
}, [api, load, t]);
|
|
268
|
+
return { available, loading, stages, create, update, remove };
|
|
269
|
+
}
|
|
270
|
+
/** The dotted phantom lane at the end of the board (Bitrix/Trello pattern). */
|
|
271
|
+
export function AddStageColumn({ onClick }) {
|
|
272
|
+
const { t } = useTranslation();
|
|
273
|
+
return (_jsxs("button", { type: "button", onClick: onClick, className: "group/add flex min-h-[55vh] min-w-[220px] max-w-[280px] shrink-0 flex-col items-center justify-center gap-2 rounded-xl border border-dashed bg-transparent p-4 text-sm text-muted-foreground transition-colors hover:border-primary/50 hover:bg-muted/40 hover:text-foreground", "data-testid": "kanban-add-stage", children: [_jsx("span", { className: "flex size-9 items-center justify-center rounded-full border border-dashed transition-colors group-hover/add:border-primary/50", children: _jsx(Plus, { className: "h-4 w-4" }) }), t('dynamic.custom_stages.add', { defaultValue: 'Agregar etapa' })] }));
|
|
274
|
+
}
|
|
275
|
+
/** The ⋮ menu shown in a custom lane's header. */
|
|
276
|
+
export function CustomStageLaneMenu({ stage, onEdit, onDelete, }) {
|
|
277
|
+
const { t } = useTranslation();
|
|
278
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": t('dynamic.custom_stages.menu', {
|
|
279
|
+
defaultValue: 'Opciones de la etapa',
|
|
280
|
+
}), "data-testid": `custom-stage-menu-${stage.key}`, children: _jsx(MoreVertical, { className: "h-3.5 w-3.5" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => onEdit(stage), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), t('dynamic.custom_stages.edit', { defaultValue: 'Editar' })] }), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => onDelete(stage), children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), t('dynamic.custom_stages.delete', { defaultValue: 'Eliminar' })] })] })] }));
|
|
281
|
+
}
|
|
282
|
+
export function CustomStageDialog({ open, onOpenChange, model, columns, initial, nextPosition, onCreate, onUpdate, }) {
|
|
283
|
+
const { t } = useTranslation();
|
|
284
|
+
const isDark = typeof document !== 'undefined' &&
|
|
285
|
+
document.documentElement.classList.contains('dark');
|
|
286
|
+
const fieldChoices = useMemo(() => customStageFilterFields(columns), [columns]);
|
|
287
|
+
const [label, setLabel] = useState('');
|
|
288
|
+
const [color, setColor] = useState(CUSTOM_STAGE_COLORS[0]);
|
|
289
|
+
const [type, setType] = useState('stage');
|
|
290
|
+
const [filters, setFilters] = useState([]);
|
|
291
|
+
const [saving, setSaving] = useState(false);
|
|
292
|
+
// Re-seed the form each time the dialog opens (create vs edit).
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (!open)
|
|
295
|
+
return;
|
|
296
|
+
if (initial) {
|
|
297
|
+
setLabel(initial.label);
|
|
298
|
+
setColor(initial.color || CUSTOM_STAGE_COLORS[0]);
|
|
299
|
+
setType(initial.type);
|
|
300
|
+
setFilters(initial.filters?.length
|
|
301
|
+
? initial.filters.map((f) => ({ ...f }))
|
|
302
|
+
: [emptyCustomStageFilter(fieldChoices[0]?.key ?? '')]);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
setLabel('');
|
|
306
|
+
setColor(CUSTOM_STAGE_COLORS[0]);
|
|
307
|
+
setType('stage');
|
|
308
|
+
setFilters([emptyCustomStageFilter(fieldChoices[0]?.key ?? '')]);
|
|
309
|
+
}
|
|
310
|
+
}, [open, initial, fieldChoices]);
|
|
311
|
+
const patchFilter = (i, patch) => setFilters((prev) => prev.map((f, idx) => (idx === i ? { ...f, ...patch } : f)));
|
|
312
|
+
const addFilter = () => setFilters((prev) => [
|
|
313
|
+
...prev,
|
|
314
|
+
emptyCustomStageFilter(fieldChoices[0]?.key ?? ''),
|
|
315
|
+
]);
|
|
316
|
+
const removeFilter = (i) => setFilters((prev) => prev.filter((_, idx) => idx !== i));
|
|
317
|
+
const valid = isCustomStageDraftValid({ label, type, filters });
|
|
318
|
+
const submit = async () => {
|
|
319
|
+
if (!valid || saving)
|
|
320
|
+
return;
|
|
321
|
+
setSaving(true);
|
|
322
|
+
// Only smart lanes carry filters; a normal stage sends an empty set.
|
|
323
|
+
const cleanFilters = type === 'smart'
|
|
324
|
+
? filters.filter((f) => f.field && String(f.value).trim() !== '')
|
|
325
|
+
: [];
|
|
326
|
+
try {
|
|
327
|
+
if (initial) {
|
|
328
|
+
// PUT only accepts label/color/position/filters/enabled — model,
|
|
329
|
+
// type and key are immutable (ops #704), so we never send them.
|
|
330
|
+
await onUpdate(initial.id, {
|
|
331
|
+
label: label.trim(),
|
|
332
|
+
color,
|
|
333
|
+
filters: cleanFilters,
|
|
334
|
+
});
|
|
335
|
+
toast.success(t('dynamic.custom_stages.updated', {
|
|
336
|
+
defaultValue: 'Etapa actualizada',
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
await onCreate({
|
|
341
|
+
model,
|
|
342
|
+
key: slugifyStageKey(label),
|
|
343
|
+
label: label.trim(),
|
|
344
|
+
color,
|
|
345
|
+
position: nextPosition,
|
|
346
|
+
type,
|
|
347
|
+
filters: cleanFilters,
|
|
348
|
+
enabled: true,
|
|
349
|
+
});
|
|
350
|
+
toast.success(t('dynamic.custom_stages.created', {
|
|
351
|
+
defaultValue: 'Etapa creada',
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
onOpenChange(false);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// toast already surfaced by the hook; keep the draft open.
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
setSaving(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: initial
|
|
364
|
+
? t('dynamic.custom_stages.edit_title', {
|
|
365
|
+
defaultValue: 'Editar etapa',
|
|
366
|
+
})
|
|
367
|
+
: t('dynamic.custom_stages.new_title', {
|
|
368
|
+
defaultValue: 'Nueva etapa',
|
|
369
|
+
}) }), _jsx(DialogDescription, { children: t('dynamic.custom_stages.dialog_description', {
|
|
370
|
+
defaultValue: 'Agrega una columna al tablero. Una etapa normal recibe tarjetas al arrastrarlas; una etapa inteligente muestra las tarjetas que cumplen condiciones.',
|
|
371
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.custom_stages.name_label', {
|
|
372
|
+
defaultValue: 'Nombre',
|
|
373
|
+
}) }), _jsx(Input, { value: label, onChange: (e) => setLabel(e.target.value), placeholder: t('dynamic.custom_stages.name_placeholder', {
|
|
374
|
+
defaultValue: 'Nombre de la etapa',
|
|
375
|
+
}), "data-testid": "custom-stage-name", autoFocus: true })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.custom_stages.color_label', {
|
|
376
|
+
defaultValue: 'Color',
|
|
377
|
+
}) }), _jsx("div", { className: "flex flex-wrap gap-1.5", "data-testid": "custom-stage-colors", children: CUSTOM_STAGE_COLORS.map((c) => {
|
|
378
|
+
const style = generateBadgeStyles(c, { isDark });
|
|
379
|
+
const selected = c === color;
|
|
380
|
+
return (_jsx("button", { type: "button", onClick: () => setColor(c), className: `size-6 rounded-full border-2 transition-transform hover:scale-110 ${selected
|
|
381
|
+
? 'border-foreground'
|
|
382
|
+
: 'border-transparent'}`, style: {
|
|
383
|
+
backgroundColor: style.backgroundColor ||
|
|
384
|
+
style.background,
|
|
385
|
+
}, "aria-label": c, "aria-pressed": selected, "data-testid": `custom-stage-color-${c}` }, c));
|
|
386
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.custom_stages.type_label', {
|
|
387
|
+
defaultValue: 'Tipo',
|
|
388
|
+
}) }), _jsxs(Select, { value: type, onValueChange: (v) => setType(v), disabled: !!initial, children: [_jsx(SelectTrigger, { "data-testid": "custom-stage-type", disabled: !!initial, children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "stage", children: t('dynamic.custom_stages.type_stage', {
|
|
389
|
+
defaultValue: 'Etapa normal',
|
|
390
|
+
}) }), _jsx(SelectItem, { value: "smart", children: t('dynamic.custom_stages.type_smart', {
|
|
391
|
+
defaultValue: 'Etapa inteligente',
|
|
392
|
+
}) })] })] })] }), type === 'smart' && (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border bg-muted/30 p-3", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [_jsx(Filter, { className: "h-3.5 w-3.5" }), t('dynamic.custom_stages.conditions_label', {
|
|
393
|
+
defaultValue: 'Condiciones',
|
|
394
|
+
})] }), filters.map((f, i) => (_jsxs("div", { className: "grid grid-cols-[1fr_auto_1fr_auto] items-center gap-1.5", "data-testid": `custom-stage-condition-${i}`, children: [_jsxs(Select, { value: f.field, onValueChange: (v) => patchFilter(i, { field: v }), children: [_jsx(SelectTrigger, { className: "h-8 text-xs", "data-testid": `custom-stage-condition-field-${i}`, children: _jsx(SelectValue, { placeholder: t('dynamic.custom_stages.field_placeholder', { defaultValue: 'Campo' }) }) }), _jsx(SelectContent, { children: fieldChoices.map((c) => (_jsx(SelectItem, { value: c.key, className: "text-xs", children: t(c.label, { defaultValue: c.label }) }, c.key))) })] }), _jsxs(Select, { value: f.op, onValueChange: (v) => patchFilter(i, { op: v }), children: [_jsx(SelectTrigger, { className: "h-8 w-[104px] text-xs", "data-testid": `custom-stage-condition-op-${i}`, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CUSTOM_STAGE_FILTER_OPS.map((op) => (_jsx(SelectItem, { value: op, className: "text-xs", children: t(`dynamic.custom_stages.op.${op}`, {
|
|
395
|
+
defaultValue: op === 'eq'
|
|
396
|
+
? 'es igual'
|
|
397
|
+
: op === 'neq'
|
|
398
|
+
? 'distinto'
|
|
399
|
+
: op === 'contains'
|
|
400
|
+
? 'contiene'
|
|
401
|
+
: 'en lista',
|
|
402
|
+
}) }, op))) })] }), _jsx(Input, { value: f.value, onChange: (e) => patchFilter(i, { value: e.target.value }), placeholder: t('dynamic.custom_stages.value_placeholder', {
|
|
403
|
+
defaultValue: 'Valor',
|
|
404
|
+
}), className: "h-8 text-xs", "data-testid": `custom-stage-condition-value-${i}` }), _jsx("button", { type: "button", onClick: () => removeFilter(i), disabled: filters.length === 1, className: "flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-destructive disabled:opacity-40", "aria-label": t('dynamic.custom_stages.remove_condition', {
|
|
405
|
+
defaultValue: 'Quitar condición',
|
|
406
|
+
}), "data-testid": `custom-stage-condition-remove-${i}`, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }, i))), _jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 justify-start gap-1 text-xs", onClick: addFilter, "data-testid": "custom-stage-add-condition", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), t('dynamic.custom_stages.add_condition', {
|
|
407
|
+
defaultValue: 'Agregar condición',
|
|
408
|
+
})] })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving, children: t('dynamic.custom_stages.cancel', { defaultValue: 'Cancelar' }) }), _jsx(Button, { onClick: () => void submit(), disabled: !valid || saving, "data-testid": "custom-stage-save", children: initial
|
|
409
|
+
? t('dynamic.custom_stages.save', { defaultValue: 'Guardar' })
|
|
410
|
+
: t('dynamic.custom_stages.create', { defaultValue: 'Crear etapa' }) })] })] }) }));
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Confirms deleting a custom lane. A 409 (the backend rejected because cards
|
|
414
|
+
* still sit on a real stage) doesn't close the dialog — it reads `meta.cards`
|
|
415
|
+
* from the response, shows the count, and offers a target lane to reassign the
|
|
416
|
+
* cards to; confirming again retries the delete with `reassign_to`.
|
|
417
|
+
*/
|
|
418
|
+
export function CustomStageDeleteDialog({ open, onOpenChange, stage, reassignTargets = [], onConfirm, }) {
|
|
419
|
+
const { t } = useTranslation();
|
|
420
|
+
const [busy, setBusy] = useState(false);
|
|
421
|
+
const [conflict, setConflict] = useState(false);
|
|
422
|
+
const [cardCount, setCardCount] = useState(null);
|
|
423
|
+
const [reassignTo, setReassignTo] = useState('');
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
if (open) {
|
|
426
|
+
setConflict(false);
|
|
427
|
+
setCardCount(null);
|
|
428
|
+
setReassignTo('');
|
|
429
|
+
}
|
|
430
|
+
}, [open]);
|
|
431
|
+
if (!stage)
|
|
432
|
+
return null;
|
|
433
|
+
const targets = reassignTargets.filter((tg) => tg.key !== stage.key);
|
|
434
|
+
const confirm = async () => {
|
|
435
|
+
// In conflict mode the delete only proceeds once a target is chosen.
|
|
436
|
+
if (conflict && !reassignTo)
|
|
437
|
+
return;
|
|
438
|
+
setBusy(true);
|
|
439
|
+
try {
|
|
440
|
+
await onConfirm(stage, conflict ? reassignTo : undefined);
|
|
441
|
+
toast.success(t('dynamic.custom_stages.deleted', {
|
|
442
|
+
defaultValue: 'Etapa eliminada',
|
|
443
|
+
}));
|
|
444
|
+
onOpenChange(false);
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
if (e?.response?.status === 409) {
|
|
448
|
+
// Cards still on the stage — surface the count and let the user
|
|
449
|
+
// pick a lane to move them to, then retry with reassign_to.
|
|
450
|
+
setCardCount(e?.response?.data?.meta?.cards ?? null);
|
|
451
|
+
setConflict(true);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
onOpenChange(false);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
setBusy(false);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('dynamic.custom_stages.delete_title', {
|
|
462
|
+
defaultValue: 'Eliminar etapa',
|
|
463
|
+
}) }), _jsx(DialogDescription, { children: conflict
|
|
464
|
+
? t('dynamic.custom_stages.delete_conflict', {
|
|
465
|
+
defaultValue: 'La etapa tiene {{count}} tarjeta(s). Elige a qué columna moverlas antes de eliminarla.',
|
|
466
|
+
count: cardCount ?? 0,
|
|
467
|
+
})
|
|
468
|
+
: t('dynamic.custom_stages.delete_confirm', {
|
|
469
|
+
defaultValue: '¿Eliminar la etapa "{{label}}"? Esta acción no se puede deshacer.',
|
|
470
|
+
label: t(stage.label, { defaultValue: stage.label }),
|
|
471
|
+
}) })] }), conflict && (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: t('dynamic.custom_stages.reassign_label', {
|
|
472
|
+
defaultValue: 'Mover tarjetas a',
|
|
473
|
+
}) }), _jsxs(Select, { value: reassignTo, onValueChange: setReassignTo, children: [_jsx(SelectTrigger, { "data-testid": "custom-stage-reassign", children: _jsx(SelectValue, { placeholder: t('dynamic.custom_stages.reassign_placeholder', { defaultValue: 'Elige una columna' }) }) }), _jsx(SelectContent, { children: targets.map((tg) => (_jsx(SelectItem, { value: tg.key, children: t(tg.label, { defaultValue: tg.label }) }, tg.key))) })] })] })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t('dynamic.custom_stages.cancel', { defaultValue: 'Cancelar' }) }), _jsx(Button, { variant: "destructive", onClick: () => void confirm(), disabled: busy || (conflict && !reassignTo), "data-testid": "custom-stage-delete-confirm", children: conflict
|
|
474
|
+
? t('dynamic.custom_stages.reassign_and_delete', {
|
|
475
|
+
defaultValue: 'Mover y eliminar',
|
|
476
|
+
})
|
|
477
|
+
: t('dynamic.custom_stages.delete', {
|
|
478
|
+
defaultValue: 'Eliminar',
|
|
479
|
+
}) })] })] }) }));
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A virtual lane defined by `filters`. It runs its OWN list query (the board's
|
|
483
|
+
* shared records don't include it), so it stays correct regardless of what the
|
|
484
|
+
* main board page loaded. Cards render read-only — a smart lane is a saved view,
|
|
485
|
+
* not a drop target — and the header carries a funnel glyph + the custom menu.
|
|
486
|
+
*/
|
|
487
|
+
export function SmartLane({ stage, model, endpoint, defaultFilters, pageSize = 50, isDark, renderCard, refreshTrigger, onEdit, onDelete, }) {
|
|
488
|
+
const { t } = useTranslation();
|
|
489
|
+
const api = useApi();
|
|
490
|
+
const [records, setRecords] = useState([]);
|
|
491
|
+
const [total, setTotal] = useState(null);
|
|
492
|
+
const [loading, setLoading] = useState(true);
|
|
493
|
+
const params = useMemo(() => smartLaneParams(stage.filters), [stage.filters]);
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
let cancelled = false;
|
|
496
|
+
setLoading(true);
|
|
497
|
+
api
|
|
498
|
+
.get(endpoint || `/data/${model}`, {
|
|
499
|
+
params: {
|
|
500
|
+
page: 1,
|
|
501
|
+
per_page: pageSize,
|
|
502
|
+
...defaultFilters,
|
|
503
|
+
...params,
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
.then((res) => {
|
|
507
|
+
if (cancelled)
|
|
508
|
+
return;
|
|
509
|
+
if (res.data.success)
|
|
510
|
+
setRecords(res.data.data || []);
|
|
511
|
+
setTotal(res.data.meta?.total ?? res.data.meta?.count ?? null);
|
|
512
|
+
})
|
|
513
|
+
.catch(() => {
|
|
514
|
+
if (!cancelled)
|
|
515
|
+
setRecords([]);
|
|
516
|
+
})
|
|
517
|
+
.finally(() => {
|
|
518
|
+
if (!cancelled)
|
|
519
|
+
setLoading(false);
|
|
520
|
+
});
|
|
521
|
+
return () => {
|
|
522
|
+
cancelled = true;
|
|
523
|
+
};
|
|
524
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
525
|
+
}, [api, endpoint, model, pageSize, JSON.stringify(params), JSON.stringify(defaultFilters ?? {}), refreshTrigger]);
|
|
526
|
+
const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
|
|
527
|
+
isDark,
|
|
528
|
+
});
|
|
529
|
+
const count = total ?? records.length;
|
|
530
|
+
return (_jsxs("div", { className: "flex min-w-[280px] max-w-[420px] flex-1 shrink-0 flex-col rounded-xl border border-dashed bg-muted/20", "data-smart-stage": stage.key, "data-testid": `smart-lane-${stage.key}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-muted-foreground", title: t('dynamic.custom_stages.smart_hint', {
|
|
531
|
+
defaultValue: 'Etapa inteligente (por condiciones)',
|
|
532
|
+
}), children: _jsx(Filter, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: count })] }), _jsx(CustomStageLaneMenu, { stage: stage, onEdit: onEdit, onDelete: onDelete })] }), _jsx("div", { className: "flex min-h-[55vh] max-h-[70vh] min-w-0 flex-col gap-2 overflow-y-auto px-2 pb-3", children: loading && records.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : records.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('dynamic.custom_stages.smart_empty', {
|
|
533
|
+
defaultValue: 'Sin tarjetas que cumplan las condiciones',
|
|
534
|
+
}) })) : (records.map((card) => (_jsx(React.Fragment, { children: renderCard(card) }, String(card.id))))) })] }));
|
|
535
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAuC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-f
|
|
|
23
23
|
import { DynamicRelations } from '../dynamic-relations';
|
|
24
24
|
import { useOptionsResolver } from '../use-options-resolver';
|
|
25
25
|
import { getFieldRef } from '../dynamic-form-schema';
|
|
26
|
+
import { FieldCell } from '../field-grid';
|
|
26
27
|
import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
|
|
27
28
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
|
|
28
29
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
@@ -501,10 +502,12 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
501
502
|
};
|
|
502
503
|
const title = modalMeta ? config.getTitle(modalMeta, t) : '';
|
|
503
504
|
const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
|
|
504
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1
|
|
505
|
-
const isFullWidth = field.type === 'textarea'
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: [visibleFields.map(field => {
|
|
506
|
+
const isFullWidth = field.type === 'textarea' ||
|
|
507
|
+
field.widget === 'textarea' ||
|
|
508
|
+
field.widget === 'richtext';
|
|
509
|
+
return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
|
|
510
|
+
}), record?.external_url && (_jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
|
|
508
511
|
}
|
|
509
512
|
function LoadingSkeleton() {
|
|
510
513
|
return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
|