@asteby/metacore-runtime-react 18.28.3 → 19.0.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 +9 -0
- package/dist/dynamic-kanban.d.ts +66 -0
- package/dist/dynamic-kanban.d.ts.map +1 -0
- package/dist/dynamic-kanban.js +341 -0
- package/dist/dynamic-view.d.ts +18 -0
- package/dist/dynamic-view.d.ts.map +1 -0
- package/dist/dynamic-view.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/dynamic-kanban.test.tsx +268 -0
- package/src/dynamic-kanban.tsx +767 -0
- package/src/dynamic-view.tsx +99 -0
- package/src/index.ts +15 -0
- package/src/types.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 19.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- feat(kanban): new `DynamicKanban` view-type renderer, a sibling of `DynamicTable` sharing the same contract (`model` + `endpoint` + injected `ApiProvider`). Reads `view_type`/`group_by` from the table metadata (RFC §1.2) and derives board lanes from the model-level `stages[]` (or, as a fallback, the `group_by` column's status `options`). Records are fetched through the same `/data/:model` path and bucketed by `row[group_by]`; cards render through the existing `ActivityValueRenderer` (title + 2-3 fields) and reuse the `ActionModalDispatcher`/`useModelActions('row')` plumbing for the per-card action menu.
|
|
8
|
+
|
|
9
|
+
Drag-to-move (via `@dnd-kit/core`) is **optimistic**: dropping a card into another lane mutates local state immediately and fires `PUT /data/:model/me/:id { <group_by>: <dest> }`; on failure the move reverts and a toast surfaces — sidestepping the "refetch loses scroll/selection" gap. When the metadata declares `transitions[]`, a card may only drop into a stage reachable from its current one (disallowed lanes dim and reject the drop; the kernel still validates server-side).
|
|
10
|
+
|
|
11
|
+
Also adds `DynamicView`, a metadata-driven dispatcher that routes `view_type === 'kanban'` → `DynamicKanban`, else → `DynamicTable`, plus the pure helpers `deriveStages`, `groupByStage`, `isTransitionAllowed`, `applyOptimisticMove`, `selectCardColumns`, `resolveViewRenderer`. New `TableMetadata` fields (`view_type`, `group_by`, `stages`, `transitions`) and `StageMeta`/`StageTransition` types are purely additive. New deps: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`.
|
|
3
12
|
## 18.28.3
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { TableMetadata, ColumnDefinition, StageMeta, StageTransition } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the board lanes for a kanban view. Prefers the model-level
|
|
5
|
+
* `metadata.stages` (the kernel's `stages[]`); falls back to the `group_by`
|
|
6
|
+
* column's `options` (the kernel projects the stage machine onto the status
|
|
7
|
+
* display). Returns lanes sorted by `order` (then declared order). Empty when
|
|
8
|
+
* neither source is present — the caller renders a "no stages" notice.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveStages(metadata: TableMetadata): StageMeta[];
|
|
11
|
+
/**
|
|
12
|
+
* Buckets records into a `stageKey → rows[]` map, one entry per declared stage
|
|
13
|
+
* (in stage order), plus a trailing `__unassigned__` bucket for rows whose
|
|
14
|
+
* stage value matches no declared lane (so nothing silently vanishes). Empty
|
|
15
|
+
* lanes are kept so the board always shows every stage.
|
|
16
|
+
*/
|
|
17
|
+
export declare const UNASSIGNED_LANE = "__unassigned__";
|
|
18
|
+
export declare function groupByStage(records: any[], groupByKey: string, stages: StageMeta[]): Map<string, any[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a card may move `from → to` given the declared transitions. No
|
|
21
|
+
* transitions declared → unrestricted (the kernel still validates server-side).
|
|
22
|
+
* A move to the same stage is always a no-op "allowed". `'*'` is a wildcard on
|
|
23
|
+
* either side.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isTransitionAllowed(transitions: StageTransition[] | undefined, from: string, to: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a NEW grouping with `cardId` moved from `fromStage` to `toStage`
|
|
28
|
+
* (appended to the destination lane). Pure — does not mutate the input map.
|
|
29
|
+
* Used by the optimistic drop handler so the board updates before the PUT
|
|
30
|
+
* resolves, and so the previous grouping can be restored on failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyOptimisticMove(grouped: Map<string, any[]>, cardId: string | number, fromStage: string, toStage: string, groupByKey: string): Map<string, any[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Picks the columns shown on a card: a `title` column (first searchable column,
|
|
35
|
+
* else first text-ish column) and up to `maxFields` secondary columns. Excludes
|
|
36
|
+
* the group_by column (it's the lane itself) and any column hidden from the
|
|
37
|
+
* table view (visibility modal/list, or `hidden`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function selectCardColumns(metadata: TableMetadata, maxFields?: number): {
|
|
40
|
+
title: ColumnDefinition | null;
|
|
41
|
+
fields: ColumnDefinition[];
|
|
42
|
+
};
|
|
43
|
+
export interface DynamicKanbanProps {
|
|
44
|
+
/** Model key as registered on the backend (e.g. "issue"). */
|
|
45
|
+
model: string;
|
|
46
|
+
/**
|
|
47
|
+
* Data endpoint base. Defaults to `/data/<model>`. The optimistic update
|
|
48
|
+
* PUTs to `<base>/me/<id>`.
|
|
49
|
+
*/
|
|
50
|
+
endpoint?: string;
|
|
51
|
+
/** Bump to force a metadata + records refetch (same contract as DynamicTable). */
|
|
52
|
+
refreshTrigger?: any;
|
|
53
|
+
/** Called when a card is clicked (outside its action menu). */
|
|
54
|
+
onCardClick?: (row: any) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Max cards fetched per lane render. Kanban shows all cards at once (no
|
|
57
|
+
* pagination UI), so it requests a single large page. Defaults to 200.
|
|
58
|
+
*/
|
|
59
|
+
pageSize?: number;
|
|
60
|
+
/** IANA timezone for datetime card fields (org config). */
|
|
61
|
+
timeZone?: string;
|
|
62
|
+
/** ISO 4217 currency for money card fields (org config). */
|
|
63
|
+
currency?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pageSize, timeZone, currency, }: DynamicKanbanProps): React.JSX.Element;
|
|
66
|
+
//# sourceMappingURL=dynamic-kanban.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAMhB;;;;;;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;;;;;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;AA8BD,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;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAc,EACd,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,qBA+RpB"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDraggable, useDroppable, } from '@dnd-kit/core';
|
|
5
|
+
import { MoreHorizontal } from 'lucide-react';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, ScrollArea, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
8
|
+
import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
|
|
9
|
+
import { useApi } from './api-context';
|
|
10
|
+
import { useMetadataCache } from './metadata-cache';
|
|
11
|
+
import { ActivityValueRenderer } from './activity-value-renderer';
|
|
12
|
+
import { ActionModalDispatcher } from './action-modal-dispatcher';
|
|
13
|
+
import { useModelActions } from './model-action-toolbar';
|
|
14
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
15
|
+
import { isColumnVisibleInTable } from './column-visibility';
|
|
16
|
+
import { isActionAllowedForRowState } from './dynamic-columns';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Pure helpers (exported for unit tests — no React, no transport)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the board lanes for a kanban view. Prefers the model-level
|
|
22
|
+
* `metadata.stages` (the kernel's `stages[]`); falls back to the `group_by`
|
|
23
|
+
* column's `options` (the kernel projects the stage machine onto the status
|
|
24
|
+
* display). Returns lanes sorted by `order` (then declared order). Empty when
|
|
25
|
+
* neither source is present — the caller renders a "no stages" notice.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveStages(metadata) {
|
|
28
|
+
const fromMeta = metadata.stages;
|
|
29
|
+
if (fromMeta && fromMeta.length > 0) {
|
|
30
|
+
return [...fromMeta].sort(sortByOrder);
|
|
31
|
+
}
|
|
32
|
+
const groupBy = metadata.group_by;
|
|
33
|
+
if (!groupBy)
|
|
34
|
+
return [];
|
|
35
|
+
const col = metadata.columns.find((c) => c.key === groupBy);
|
|
36
|
+
const opts = col?.options ?? [];
|
|
37
|
+
return opts.map((o, i) => ({
|
|
38
|
+
key: String(o.value),
|
|
39
|
+
label: o.label,
|
|
40
|
+
color: o.color,
|
|
41
|
+
order: i,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function sortByOrder(a, b) {
|
|
45
|
+
const ao = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
46
|
+
const bo = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
47
|
+
return ao - bo;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Buckets records into a `stageKey → rows[]` map, one entry per declared stage
|
|
51
|
+
* (in stage order), plus a trailing `__unassigned__` bucket for rows whose
|
|
52
|
+
* stage value matches no declared lane (so nothing silently vanishes). Empty
|
|
53
|
+
* lanes are kept so the board always shows every stage.
|
|
54
|
+
*/
|
|
55
|
+
export const UNASSIGNED_LANE = '__unassigned__';
|
|
56
|
+
export function groupByStage(records, groupByKey, stages) {
|
|
57
|
+
const map = new Map();
|
|
58
|
+
for (const s of stages)
|
|
59
|
+
map.set(s.key, []);
|
|
60
|
+
const known = new Set(stages.map((s) => s.key));
|
|
61
|
+
for (const row of records) {
|
|
62
|
+
const raw = row?.[groupByKey];
|
|
63
|
+
const key = raw === null || raw === undefined ? '' : String(raw);
|
|
64
|
+
if (known.has(key)) {
|
|
65
|
+
map.get(key).push(row);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (!map.has(UNASSIGNED_LANE))
|
|
69
|
+
map.set(UNASSIGNED_LANE, []);
|
|
70
|
+
map.get(UNASSIGNED_LANE).push(row);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether a card may move `from → to` given the declared transitions. No
|
|
77
|
+
* transitions declared → unrestricted (the kernel still validates server-side).
|
|
78
|
+
* A move to the same stage is always a no-op "allowed". `'*'` is a wildcard on
|
|
79
|
+
* either side.
|
|
80
|
+
*/
|
|
81
|
+
export function isTransitionAllowed(transitions, from, to) {
|
|
82
|
+
if (from === to)
|
|
83
|
+
return true;
|
|
84
|
+
if (!transitions || transitions.length === 0)
|
|
85
|
+
return true;
|
|
86
|
+
return transitions.some((t) => (t.from === from || t.from === '*') && (t.to === to || t.to === '*'));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns a NEW grouping with `cardId` moved from `fromStage` to `toStage`
|
|
90
|
+
* (appended to the destination lane). Pure — does not mutate the input map.
|
|
91
|
+
* Used by the optimistic drop handler so the board updates before the PUT
|
|
92
|
+
* resolves, and so the previous grouping can be restored on failure.
|
|
93
|
+
*/
|
|
94
|
+
export function applyOptimisticMove(grouped, cardId, fromStage, toStage, groupByKey) {
|
|
95
|
+
const next = new Map();
|
|
96
|
+
for (const [k, rows] of grouped)
|
|
97
|
+
next.set(k, [...rows]);
|
|
98
|
+
const fromRows = next.get(fromStage) ?? [];
|
|
99
|
+
const idx = fromRows.findIndex((r) => String(r.id) === String(cardId));
|
|
100
|
+
if (idx === -1)
|
|
101
|
+
return next;
|
|
102
|
+
const [moved] = fromRows.splice(idx, 1);
|
|
103
|
+
const updated = { ...moved, [groupByKey]: toStage };
|
|
104
|
+
const toRows = next.get(toStage) ?? [];
|
|
105
|
+
toRows.push(updated);
|
|
106
|
+
next.set(toStage, toRows);
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Picks the columns shown on a card: a `title` column (first searchable column,
|
|
111
|
+
* else first text-ish column) and up to `maxFields` secondary columns. Excludes
|
|
112
|
+
* the group_by column (it's the lane itself) and any column hidden from the
|
|
113
|
+
* table view (visibility modal/list, or `hidden`).
|
|
114
|
+
*/
|
|
115
|
+
export function selectCardColumns(metadata, maxFields = 3) {
|
|
116
|
+
const groupBy = metadata.group_by;
|
|
117
|
+
const visible = metadata.columns.filter((c) => c.key !== groupBy &&
|
|
118
|
+
!c.hidden &&
|
|
119
|
+
isColumnVisibleInTable(c) &&
|
|
120
|
+
c.key !== 'id');
|
|
121
|
+
const title = visible.find((c) => c.searchable) ??
|
|
122
|
+
visible.find((c) => c.type === 'text' || c.cellStyle === 'truncate-text') ??
|
|
123
|
+
visible[0] ??
|
|
124
|
+
null;
|
|
125
|
+
const fields = visible
|
|
126
|
+
.filter((c) => c.key !== title?.key)
|
|
127
|
+
.slice(0, maxFields);
|
|
128
|
+
return { title, fields };
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Theme hook (mirrors the private one in dynamic-columns / activity-renderer)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
function useIsDarkTheme() {
|
|
134
|
+
const [isDark, setIsDark] = useState(() => typeof document !== 'undefined' &&
|
|
135
|
+
document.documentElement.classList.contains('dark'));
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (typeof document === 'undefined')
|
|
138
|
+
return;
|
|
139
|
+
const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
|
|
140
|
+
const observer = new MutationObserver(sync);
|
|
141
|
+
observer.observe(document.documentElement, {
|
|
142
|
+
attributes: true,
|
|
143
|
+
attributeFilter: ['class'],
|
|
144
|
+
});
|
|
145
|
+
return () => observer.disconnect();
|
|
146
|
+
}, []);
|
|
147
|
+
return isDark;
|
|
148
|
+
}
|
|
149
|
+
export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pageSize = 200, timeZone, currency, }) {
|
|
150
|
+
const { t, i18n } = useTranslation();
|
|
151
|
+
const api = useApi();
|
|
152
|
+
const isDark = useIsDarkTheme();
|
|
153
|
+
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
|
|
154
|
+
const cachedMeta = getMetadata(model);
|
|
155
|
+
const [metadata, setMetadata] = useState(cachedMeta || null);
|
|
156
|
+
const [records, setRecords] = useState([]);
|
|
157
|
+
const [loading, setLoading] = useState(!cachedMeta);
|
|
158
|
+
const [loadingData, setLoadingData] = useState(true);
|
|
159
|
+
// Active drag card id (for the DragOverlay + drop-zone highlighting).
|
|
160
|
+
const [activeId, setActiveId] = useState(null);
|
|
161
|
+
const [actionModal, setActionModal] = useState({ action: null, record: null });
|
|
162
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
|
|
163
|
+
// ---- metadata fetch (same path as DynamicTable) ----
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
let cancelled = false;
|
|
166
|
+
const cached = getMetadata(model);
|
|
167
|
+
if (cached) {
|
|
168
|
+
setMetadata(cached);
|
|
169
|
+
setLoading(false);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
setLoading(true);
|
|
173
|
+
}
|
|
174
|
+
api
|
|
175
|
+
.get(`/metadata/table/${model}`)
|
|
176
|
+
.then((res) => {
|
|
177
|
+
if (cancelled)
|
|
178
|
+
return;
|
|
179
|
+
const body = res.data;
|
|
180
|
+
if (body.success) {
|
|
181
|
+
setMetadata(body.data);
|
|
182
|
+
cacheMetadata(model, body.data);
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch((err) => {
|
|
186
|
+
if (!cancelled && !cached)
|
|
187
|
+
console.error('Error al cargar la configuración del tablero', err);
|
|
188
|
+
})
|
|
189
|
+
.finally(() => {
|
|
190
|
+
if (!cancelled)
|
|
191
|
+
setLoading(false);
|
|
192
|
+
});
|
|
193
|
+
return () => {
|
|
194
|
+
cancelled = true;
|
|
195
|
+
};
|
|
196
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
197
|
+
}, [model]);
|
|
198
|
+
// ---- records fetch (same path as DynamicTable, single large page) ----
|
|
199
|
+
const fetchData = useCallback(async () => {
|
|
200
|
+
if (!metadata)
|
|
201
|
+
return;
|
|
202
|
+
setLoadingData(true);
|
|
203
|
+
try {
|
|
204
|
+
const res = (await api.get(endpoint || `/data/${model}`, {
|
|
205
|
+
params: { page: 1, per_page: pageSize },
|
|
206
|
+
}));
|
|
207
|
+
if (res.data.success)
|
|
208
|
+
setRecords(res.data.data || []);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.error('Error al cargar las tarjetas', err);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
setLoadingData(false);
|
|
215
|
+
}
|
|
216
|
+
}, [api, endpoint, model, metadata, pageSize]);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (metadata)
|
|
219
|
+
void fetchData();
|
|
220
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
221
|
+
}, [metadata, refreshTrigger]);
|
|
222
|
+
const stages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
|
|
223
|
+
const groupByKey = metadata?.group_by || '';
|
|
224
|
+
const transitions = metadata?.transitions;
|
|
225
|
+
const grouped = useMemo(() => groupByStage(records, groupByKey, stages), [records, groupByKey, stages]);
|
|
226
|
+
const { title: titleCol, fields: fieldCols } = useMemo(() => (metadata ? selectCardColumns(metadata) : { title: null, fields: [] }), [metadata]);
|
|
227
|
+
// Row-placement actions reused verbatim from the table's plumbing.
|
|
228
|
+
const rowActions = useModelActions(model, ['row'], metadata?.actions);
|
|
229
|
+
const cardById = useMemo(() => {
|
|
230
|
+
const m = new Map();
|
|
231
|
+
for (const r of records)
|
|
232
|
+
m.set(String(r.id), r);
|
|
233
|
+
return m;
|
|
234
|
+
}, [records]);
|
|
235
|
+
const stageOfCard = useCallback((id) => {
|
|
236
|
+
const card = cardById.get(id);
|
|
237
|
+
const raw = card?.[groupByKey];
|
|
238
|
+
return raw === null || raw === undefined ? '' : String(raw);
|
|
239
|
+
}, [cardById, groupByKey]);
|
|
240
|
+
const onDragStart = useCallback((e) => {
|
|
241
|
+
setActiveId(String(e.active.id));
|
|
242
|
+
}, []);
|
|
243
|
+
const onDragEnd = useCallback(async (e) => {
|
|
244
|
+
setActiveId(null);
|
|
245
|
+
const { active, over } = e;
|
|
246
|
+
if (!over)
|
|
247
|
+
return;
|
|
248
|
+
const cardId = String(active.id);
|
|
249
|
+
const destStage = String(over.id);
|
|
250
|
+
const srcStage = stageOfCard(cardId);
|
|
251
|
+
if (srcStage === destStage)
|
|
252
|
+
return;
|
|
253
|
+
if (!isTransitionAllowed(transitions, srcStage, destStage)) {
|
|
254
|
+
toast.error(t('kanban.invalidTransition', {
|
|
255
|
+
defaultValue: 'Movimiento no permitido entre estas etapas',
|
|
256
|
+
}));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// OPTIMISTIC: move the card in local state immediately.
|
|
260
|
+
const prevRecords = records;
|
|
261
|
+
setRecords((rs) => rs.map((r) => String(r.id) === cardId ? { ...r, [groupByKey]: destStage } : r));
|
|
262
|
+
try {
|
|
263
|
+
const base = endpoint || `/data/${model}`;
|
|
264
|
+
const res = (await api.put(`${base}/me/${cardId}`, {
|
|
265
|
+
[groupByKey]: destStage,
|
|
266
|
+
}));
|
|
267
|
+
if (res?.data && res.data.success === false) {
|
|
268
|
+
throw new Error(res.data.message || 'update_failed');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
// REVERT + toast on failure.
|
|
273
|
+
setRecords(prevRecords);
|
|
274
|
+
toast.error(t('kanban.moveFailed', {
|
|
275
|
+
defaultValue: 'No se pudo mover la tarjeta',
|
|
276
|
+
}) +
|
|
277
|
+
(err?.response?.data?.message
|
|
278
|
+
? `: ${err.response.data.message}`
|
|
279
|
+
: ''));
|
|
280
|
+
}
|
|
281
|
+
}, [api, endpoint, groupByKey, model, records, stageOfCard, t, transitions]);
|
|
282
|
+
if (loading) {
|
|
283
|
+
return (_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", children: [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "w-72 shrink-0 space-y-3", children: [_jsx(Skeleton, { className: "h-8 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" })] }, i))) }));
|
|
284
|
+
}
|
|
285
|
+
if (!metadata || !groupByKey || stages.length === 0) {
|
|
286
|
+
return (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground", children: t('kanban.noStages', {
|
|
287
|
+
defaultValue: 'Este modelo no declara etapas para la vista de tablero.',
|
|
288
|
+
}) }));
|
|
289
|
+
}
|
|
290
|
+
const activeCard = activeId ? cardById.get(activeId) : null;
|
|
291
|
+
const activeStage = activeId ? stageOfCard(activeId) : '';
|
|
292
|
+
const lanes = [...stages];
|
|
293
|
+
if (grouped.has(UNASSIGNED_LANE)) {
|
|
294
|
+
lanes.push({
|
|
295
|
+
key: UNASSIGNED_LANE,
|
|
296
|
+
label: t('kanban.unassigned', { defaultValue: 'Sin etapa' }),
|
|
297
|
+
color: 'slate',
|
|
298
|
+
order: Number.MAX_SAFE_INTEGER,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return (_jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
|
|
302
|
+
const cards = grouped.get(stage.key) ?? [];
|
|
303
|
+
const droppableAllowed = !activeId ||
|
|
304
|
+
stage.key === activeStage ||
|
|
305
|
+
isTransitionAllowed(transitions, activeStage, stage.key);
|
|
306
|
+
return (_jsx(KanbanLane, { stage: stage, count: cards.length, isDark: isDark, dimmed: !!activeId && !droppableAllowed, disabled: !!activeId && !droppableAllowed, children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: (action, record) => setActionModal({ action, record }) }, String(card.id))))) }, stage.key));
|
|
307
|
+
}) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), actionModal.action && (_jsx(ActionModalDispatcher, { open: !!actionModal.action, onOpenChange: (open) => {
|
|
308
|
+
if (!open)
|
|
309
|
+
setActionModal({ action: null, record: null });
|
|
310
|
+
}, action: actionModal.action, model: model, record: actionModal.record ?? {}, endpoint: endpoint ?? `/data/${model}/me`, onSuccess: () => {
|
|
311
|
+
setActionModal({ action: null, record: null });
|
|
312
|
+
void fetchData();
|
|
313
|
+
} }))] }));
|
|
314
|
+
}
|
|
315
|
+
function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
|
|
316
|
+
const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
|
|
317
|
+
const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
|
|
318
|
+
isDark,
|
|
319
|
+
});
|
|
320
|
+
return (_jsxs("div", { ref: setNodeRef, className: "flex w-72 shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
|
|
321
|
+
opacity: dimmed ? 0.45 : 1,
|
|
322
|
+
outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
|
|
323
|
+
outlineOffset: 2,
|
|
324
|
+
}, "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: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: stage.label }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: count })] }), _jsx(ScrollArea, { className: "max-h-[70vh]", children: _jsx("div", { className: "flex flex-col gap-2 px-2 pb-3", children: children }) })] }));
|
|
325
|
+
}
|
|
326
|
+
function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
|
|
327
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
328
|
+
id: String(card.id),
|
|
329
|
+
});
|
|
330
|
+
const visibleActions = actions.filter((a) => isActionAllowedForRowState(a, card));
|
|
331
|
+
return (_jsx(Card, { ref: setNodeRef, ...attributes, ...listeners, className: "cursor-grab active:cursor-grabbing border-border/70 shadow-sm", style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 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",
|
|
332
|
+
// Don't start a drag / card click from the menu button.
|
|
333
|
+
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) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
onAction(a, card);
|
|
336
|
+
}, children: [_jsx(DynamicIcon, { name: a.icon || 'Zap', className: "mr-2 h-4 w-4" }), a.label] }, a.key))) })] }))] }), fieldCols.map((col) => (_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
|
|
337
|
+
}
|
|
338
|
+
// Static preview rendered inside the DragOverlay (no dnd hooks, no menu).
|
|
339
|
+
function CardPreview({ card, titleCol, fieldCols, locale, timeZone, currency, }) {
|
|
340
|
+
return (_jsx(Card, { className: "w-72 cursor-grabbing border-primary/40 shadow-lg", children: _jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsx("div", { className: "text-sm font-medium leading-snug", children: titleCol ? (_jsx(ActivityValueRenderer, { value: card[titleCol.key], col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (String(card.id)) }), fieldCols.map((col) => (_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ActivityValueRenderer, { value: card[col.key], col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }) }));
|
|
341
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DynamicTableProps } from './dynamic-table';
|
|
2
|
+
import { type DynamicKanbanProps } from './dynamic-kanban';
|
|
3
|
+
/**
|
|
4
|
+
* Pure routing decision: which renderer a `view_type` maps onto. Exported so a
|
|
5
|
+
* host that resolves metadata itself can branch without mounting this wrapper.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveViewRenderer(viewType: string | undefined): 'kanban' | 'table';
|
|
8
|
+
export interface DynamicViewProps extends DynamicTableProps {
|
|
9
|
+
/**
|
|
10
|
+
* Props forwarded to <DynamicKanban> when the model resolves to a kanban
|
|
11
|
+
* view. `model`/`endpoint`/`refreshTrigger`/`timeZone`/`currency` are shared
|
|
12
|
+
* with the table props and forwarded automatically; this is for the
|
|
13
|
+
* kanban-only extras (e.g. `onCardClick`, `pageSize`).
|
|
14
|
+
*/
|
|
15
|
+
kanbanProps?: Partial<Omit<DynamicKanbanProps, 'model' | 'endpoint'>>;
|
|
16
|
+
}
|
|
17
|
+
export declare function DynamicView({ kanbanProps, ...tableProps }: DynamicViewProps): import("react").JSX.Element | null;
|
|
18
|
+
//# sourceMappingURL=dynamic-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-view.d.ts","sourceRoot":"","sources":["../src/dynamic-view.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACtE,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAGzE;;;GAGG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC7B,QAAQ,GAAG,OAAO,CAEpB;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACvD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC,CAAA;CACxE;AAED,wBAAgB,WAAW,CAAC,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE,EAAE,gBAAgB,sCAwD3E"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// DynamicView — the single entry point a host renders for a model's "list"
|
|
3
|
+
// surface. It picks the renderer from the model's `view_type`:
|
|
4
|
+
// - `'kanban'` → <DynamicKanban>
|
|
5
|
+
// - anything else / absent → <DynamicTable> (the default)
|
|
6
|
+
//
|
|
7
|
+
// The decision is metadata-driven (RFC §1.2): the kernel serves `view_type` +
|
|
8
|
+
// `group_by` on the table metadata, derived from the nav item. A host that
|
|
9
|
+
// already knows the view type can skip this and render the concrete component
|
|
10
|
+
// directly; a generic host route (e.g. ops `/m/$model`) mounts <DynamicView>
|
|
11
|
+
// and lets the metadata decide, so the same model can expose a `table` nav and
|
|
12
|
+
// a `kanban` nav with no host code change.
|
|
13
|
+
//
|
|
14
|
+
// Both child components fetch their own metadata (cache-backed), so the extra
|
|
15
|
+
// read this wrapper does to learn `view_type` is served from the same cache —
|
|
16
|
+
// no duplicate network round-trip in practice.
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import { useApi } from './api-context';
|
|
19
|
+
import { useMetadataCache } from './metadata-cache';
|
|
20
|
+
import { DynamicTable } from './dynamic-table';
|
|
21
|
+
import { DynamicKanban } from './dynamic-kanban';
|
|
22
|
+
/**
|
|
23
|
+
* Pure routing decision: which renderer a `view_type` maps onto. Exported so a
|
|
24
|
+
* host that resolves metadata itself can branch without mounting this wrapper.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveViewRenderer(viewType) {
|
|
27
|
+
return viewType === 'kanban' ? 'kanban' : 'table';
|
|
28
|
+
}
|
|
29
|
+
export function DynamicView({ kanbanProps, ...tableProps }) {
|
|
30
|
+
const { model, endpoint, refreshTrigger, timeZone, currency } = tableProps;
|
|
31
|
+
const api = useApi();
|
|
32
|
+
const cached = useMetadataCache((s) => s.getMetadata(model));
|
|
33
|
+
const setMeta = useMetadataCache((s) => s.setMetadata);
|
|
34
|
+
const [viewType, setViewType] = useState(cached?.view_type);
|
|
35
|
+
const [resolved, setResolved] = useState(!!cached);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
const c = useMetadataCache.getState().getMetadata(model);
|
|
39
|
+
if (c) {
|
|
40
|
+
setViewType(c.view_type);
|
|
41
|
+
setResolved(true);
|
|
42
|
+
}
|
|
43
|
+
api
|
|
44
|
+
.get(`/metadata/table/${model}`)
|
|
45
|
+
.then((res) => {
|
|
46
|
+
if (cancelled)
|
|
47
|
+
return;
|
|
48
|
+
const body = res.data;
|
|
49
|
+
const meta = body?.success ? body.data : res.data;
|
|
50
|
+
if (meta) {
|
|
51
|
+
setViewType(meta.view_type);
|
|
52
|
+
setMeta(model, meta);
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch(() => {
|
|
56
|
+
/* fall back to the table renderer */
|
|
57
|
+
})
|
|
58
|
+
.finally(() => {
|
|
59
|
+
if (!cancelled)
|
|
60
|
+
setResolved(true);
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
cancelled = true;
|
|
64
|
+
};
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [model]);
|
|
67
|
+
// Until we know the view type, render nothing transient-heavy: default to the
|
|
68
|
+
// table renderer only once resolved to avoid a table→kanban flash.
|
|
69
|
+
if (!resolved && !cached)
|
|
70
|
+
return null;
|
|
71
|
+
if (resolveViewRenderer(viewType) === 'kanban') {
|
|
72
|
+
return (_jsx(DynamicKanban, { model: model, endpoint: endpoint, refreshTrigger: refreshTrigger, timeZone: timeZone, currency: currency, ...kanbanProps }));
|
|
73
|
+
}
|
|
74
|
+
return _jsx(DynamicTable, { ...tableProps });
|
|
75
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export * from './options-context';
|
|
3
3
|
export * from './dynamic-table';
|
|
4
|
+
export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
5
|
+
export { DynamicView, resolveViewRenderer, type DynamicViewProps, } from './dynamic-view';
|
|
4
6
|
export * from './dynamic-form';
|
|
5
7
|
export { ActionModalDispatcher, type ActionModalProps, } from './action-modal-dispatcher';
|
|
6
8
|
export { ModelActionToolbar, useModelActions, type ModelActionToolbarProps, type ActionPlacement, } from './model-action-toolbar';
|
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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
export * from './types';
|
|
7
7
|
export * from './options-context';
|
|
8
8
|
export * from './dynamic-table';
|
|
9
|
+
export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
10
|
+
export { DynamicView, resolveViewRenderer, } from './dynamic-view';
|
|
9
11
|
export * from './dynamic-form';
|
|
10
12
|
export { ActionModalDispatcher, } from './action-modal-dispatcher';
|
|
11
13
|
export { ModelActionToolbar, useModelActions, } from './model-action-toolbar';
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,52 @@ export interface TableMetadata {
|
|
|
19
19
|
* and attachments. Absent on hosts/older kernels — purely additive.
|
|
20
20
|
*/
|
|
21
21
|
relations?: RelationMeta[];
|
|
22
|
+
/**
|
|
23
|
+
* Which renderer the host should use for this view. `'table'` (default, or
|
|
24
|
+
* absent) → `DynamicTable`; `'kanban'` → `DynamicKanban`. Served by the
|
|
25
|
+
* kernel from the nav item's `view_type` (RFC §1.2). Purely additive — older
|
|
26
|
+
* kernels omit it and the SDK falls back to the table renderer.
|
|
27
|
+
*/
|
|
28
|
+
view_type?: 'table' | 'kanban' | (string & {});
|
|
29
|
+
/**
|
|
30
|
+
* Column key the board groups by when `view_type === 'kanban'` (the stage
|
|
31
|
+
* column, e.g. `'stage'`). Each distinct value of this column becomes a board
|
|
32
|
+
* lane. Mirrors the nav item's `group_by` (RFC §1.2).
|
|
33
|
+
*/
|
|
34
|
+
group_by?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Board lanes (the stage machine of the `group_by`/`stage_field` column).
|
|
37
|
+
* When present the kanban renders one lane per stage in `order`. When absent
|
|
38
|
+
* the SDK derives lanes from the `group_by` column's `options` (the kernel
|
|
39
|
+
* already projects `stages[]` onto the status display — RFC §1.1). Snake_case
|
|
40
|
+
* keys as the kernel serves them.
|
|
41
|
+
*/
|
|
42
|
+
stages?: StageMeta[];
|
|
43
|
+
/**
|
|
44
|
+
* Allowed stage transitions (RFC §1.1). When present, the kanban only lets a
|
|
45
|
+
* card drop into a lane reachable from its current stage; disallowed lanes
|
|
46
|
+
* are dimmed and reject the drop. `from`/`to` accept `'*'` as a wildcard.
|
|
47
|
+
* Absent → any move is allowed (the kernel still validates server-side).
|
|
48
|
+
*/
|
|
49
|
+
transitions?: StageTransition[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One board lane / pipeline stage. Mirrors the kernel v3 `Stage` (RFC §1.1).
|
|
53
|
+
* `color` is a semantic palette name (`'slate'`, `'blue'`, `'amber'`, `'green'`)
|
|
54
|
+
* or a hex literal — resolved through the same `generateBadgeStyles` helper as
|
|
55
|
+
* option badges. `is_final` flags a terminal stage (e.g. "Done").
|
|
56
|
+
*/
|
|
57
|
+
export interface StageMeta {
|
|
58
|
+
key: string;
|
|
59
|
+
label: string;
|
|
60
|
+
color?: string;
|
|
61
|
+
order?: number;
|
|
62
|
+
is_final?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Allowed `from → to` stage transition (RFC §1.1). `'*'` is a wildcard. */
|
|
65
|
+
export interface StageTransition {
|
|
66
|
+
from: string;
|
|
67
|
+
to: string;
|
|
22
68
|
}
|
|
23
69
|
/**
|
|
24
70
|
* Describes one child relation of a parent model, mirroring the kernel
|