@asteby/metacore-runtime-react 23.6.0 → 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.
@@ -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":"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"}
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 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
505
- const isFullWidth = field.type === 'textarea';
506
- return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
507
- }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", 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] }))] })] })] }) }));
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":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA0E9B,OAAO,EAEH,qBAAqB,EACrB,qBAAqB,EACxB,MAAM,gBAAgB,CAAA;AASvB,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAIhB,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,CAAA;AAMvD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAejE;AAQD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,wBAAgB,YAAY,CACxB,OAAO,EAAE,GAAG,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAepB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,eAAe,EAAE,GAAG,SAAS,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACX,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAYpB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACpE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAWnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,UAAU,EAAE,OAAO,GACpB,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,aAAa,EACvB,SAAS,SAAI,GACd;IAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkBhE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,GAAG,EACT,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACzE,OAAO,CAUT;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACxD,MAAM,CAIR;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,GAAG,EACT,IAAI,EAAE,gBAAgB,EAAE,EACxB,KAAK,EAAE,MAAM,GACd,OAAO,CAQT;AAwDD,MAAM,WAAW,kBAAkB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACvC;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,EACR,QAAa,EACb,YAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,cAAc,GACjB,EAAE,kBAAkB,qBAoyBpB"}