@asteby/metacore-runtime-react 18.28.3 → 20.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 +43 -0
- package/dist/dynamic-kanban.d.ts +66 -0
- package/dist/dynamic-kanban.d.ts.map +1 -0
- package/dist/dynamic-kanban.js +342 -0
- package/dist/dynamic-view.d.ts +42 -0
- package/dist/dynamic-view.d.ts.map +1 -0
- package/dist/dynamic-view.js +106 -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 +8 -5
- package/src/__tests__/dynamic-kanban.test.tsx +268 -0
- package/src/__tests__/dynamic-view.test.tsx +62 -0
- package/src/dynamic-kanban.tsx +768 -0
- package/src/dynamic-view.tsx +146 -0
- package/src/index.ts +17 -0
- package/src/types.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 20.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dcd95c3: DynamicKanban: traduce el label de cada etapa via i18n (`t(stage.label)` con fallback al valor crudo) — antes mostraba la key cruda (ej. `integration_github.stage.backlog`) en vez de "Backlog". Y da min-height a las lanes para que el scroll horizontal del board quede abajo en vez de flotar cuando las columnas están vacías.
|
|
8
|
+
- 3f41073: Sidebar nav: exact, view-aware active-state so sibling navs over the same model light up one at a time
|
|
9
|
+
|
|
10
|
+
The `NavGroup` active-state matcher (`checkIsActive`) now treats `view`/`group_by`
|
|
11
|
+
query params as the _identity_ of a view-style nav item. Two navs over the same
|
|
12
|
+
model that differ only by their view — e.g. a "Board" (`?view=kanban&group_by=stage`)
|
|
13
|
+
and an "Issues" (`?view=list`, or a query-less default list) — are mutually
|
|
14
|
+
exclusive: only the item whose view identity equals `currentHref` stays active,
|
|
15
|
+
fixing the bug where both lit up at once.
|
|
16
|
+
- `@asteby/metacore-ui`: the matcher is extracted into a pure, React-free
|
|
17
|
+
`layout/nav-active` module (`checkIsActive`, `splitHref`, `declaredFiltersMatch`,
|
|
18
|
+
`VIEW_PARAMS`) and re-exported from `@asteby/metacore-ui/layout` for hosts and
|
|
19
|
+
unit tests. `f_` filter and transient (page/sort/search) highlight behaviour is
|
|
20
|
+
unchanged — a query-less link still highlights under filters/pagination, and
|
|
21
|
+
per-status entries still light up one at a time.
|
|
22
|
+
- `@asteby/metacore-starter-core`: the scaffold's `nav-group` matcher gains the
|
|
23
|
+
same view/query/filter-aware logic.
|
|
24
|
+
- `@asteby/metacore-runtime-react`: `DynamicView` now reads the active view from
|
|
25
|
+
the per-nav signal — an explicit `view` prop (host router) or the `?view=`
|
|
26
|
+
query — and prefers it over the model-level `metadata.view_type`, so the same
|
|
27
|
+
model can route `?view=kanban` to `DynamicKanban` and `?view=list` to
|
|
28
|
+
`DynamicTable` with no per-model metadata change. New pure helpers
|
|
29
|
+
`readViewFromSearch` / `resolveActiveView` are exported.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [3f41073]
|
|
34
|
+
- @asteby/metacore-ui@2.6.0
|
|
35
|
+
|
|
36
|
+
## 19.0.0
|
|
37
|
+
|
|
38
|
+
### Major Changes
|
|
39
|
+
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
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).
|
|
43
|
+
|
|
44
|
+
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`.
|
|
45
|
+
|
|
3
46
|
## 18.28.3
|
|
4
47
|
|
|
5
48
|
### 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,342 @@
|
|
|
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 { t } = useTranslation();
|
|
317
|
+
const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
|
|
318
|
+
const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
|
|
319
|
+
isDark,
|
|
320
|
+
});
|
|
321
|
+
return (_jsxs("div", { ref: setNodeRef, className: "flex w-72 shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
|
|
322
|
+
opacity: dimmed ? 0.45 : 1,
|
|
323
|
+
outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
|
|
324
|
+
outlineOffset: 2,
|
|
325
|
+
}, "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: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: count })] }), _jsx(ScrollArea, { className: "min-h-[55vh] max-h-[70vh]", children: _jsx("div", { className: "flex flex-col gap-2 px-2 pb-3", children: children }) })] }));
|
|
326
|
+
}
|
|
327
|
+
function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
|
|
328
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
329
|
+
id: String(card.id),
|
|
330
|
+
});
|
|
331
|
+
const visibleActions = actions.filter((a) => isActionAllowedForRowState(a, card));
|
|
332
|
+
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",
|
|
333
|
+
// Don't start a drag / card click from the menu button.
|
|
334
|
+
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) => {
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
onAction(a, card);
|
|
337
|
+
}, 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)))] }) }));
|
|
338
|
+
}
|
|
339
|
+
// Static preview rendered inside the DragOverlay (no dnd hooks, no menu).
|
|
340
|
+
function CardPreview({ card, titleCol, fieldCols, locale, timeZone, currency, }) {
|
|
341
|
+
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)))] }) }));
|
|
342
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Reads the `view` selector out of a URL search string (`?view=kanban`, a bare
|
|
10
|
+
* `view=kanban`, or a full href). Returns `undefined` when absent. The query is
|
|
11
|
+
* the per-NAV signal: the same model exposes a "Board" nav (`?view=kanban`) and
|
|
12
|
+
* an "Issues" nav (`?view=list`), so the query — not the model-level
|
|
13
|
+
* `metadata.view_type` — decides which surface to paint. SSR-safe.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readViewFromSearch(search?: string): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the effective view selector with the right precedence:
|
|
18
|
+
* 1. an explicit `view` prop the host passes (it owns the router), then
|
|
19
|
+
* 2. the `view` query param, then
|
|
20
|
+
* 3. the model-level `metadata.view_type` default.
|
|
21
|
+
* Exported pure for unit tests and host reuse.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveActiveView(explicit: string | undefined, search: string | undefined, metadataViewType: string | undefined): string | undefined;
|
|
24
|
+
export interface DynamicViewProps extends DynamicTableProps {
|
|
25
|
+
/**
|
|
26
|
+
* Explicit view selector from the host's router (e.g. the `view` search
|
|
27
|
+
* param resolved by tanstack-router). Takes precedence over the query string
|
|
28
|
+
* and the model's `metadata.view_type`. Pass this when the host owns routing
|
|
29
|
+
* so the same model can show `?view=kanban` (board) or `?view=list` (table)
|
|
30
|
+
* with no per-model metadata change.
|
|
31
|
+
*/
|
|
32
|
+
view?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Props forwarded to <DynamicKanban> when the model resolves to a kanban
|
|
35
|
+
* view. `model`/`endpoint`/`refreshTrigger`/`timeZone`/`currency` are shared
|
|
36
|
+
* with the table props and forwarded automatically; this is for the
|
|
37
|
+
* kanban-only extras (e.g. `onCardClick`, `pageSize`).
|
|
38
|
+
*/
|
|
39
|
+
kanbanProps?: Partial<Omit<DynamicKanbanProps, 'model' | 'endpoint'>>;
|
|
40
|
+
}
|
|
41
|
+
export declare function DynamicView({ view, kanbanProps, ...tableProps }: DynamicViewProps): import("react").JSX.Element | null;
|
|
42
|
+
//# 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;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMtE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,GAAG,SAAS,CAEpB;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACvD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC,CAAA;CACxE;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE,EAAE,gBAAgB,sCAiEjF"}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
/**
|
|
30
|
+
* Reads the `view` selector out of a URL search string (`?view=kanban`, a bare
|
|
31
|
+
* `view=kanban`, or a full href). Returns `undefined` when absent. The query is
|
|
32
|
+
* the per-NAV signal: the same model exposes a "Board" nav (`?view=kanban`) and
|
|
33
|
+
* an "Issues" nav (`?view=list`), so the query — not the model-level
|
|
34
|
+
* `metadata.view_type` — decides which surface to paint. SSR-safe.
|
|
35
|
+
*/
|
|
36
|
+
export function readViewFromSearch(search) {
|
|
37
|
+
if (!search)
|
|
38
|
+
return undefined;
|
|
39
|
+
const qIndex = search.indexOf('?');
|
|
40
|
+
const qs = qIndex === -1 ? search : search.slice(qIndex + 1);
|
|
41
|
+
const v = new URLSearchParams(qs).get('view');
|
|
42
|
+
return v ?? undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the effective view selector with the right precedence:
|
|
46
|
+
* 1. an explicit `view` prop the host passes (it owns the router), then
|
|
47
|
+
* 2. the `view` query param, then
|
|
48
|
+
* 3. the model-level `metadata.view_type` default.
|
|
49
|
+
* Exported pure for unit tests and host reuse.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveActiveView(explicit, search, metadataViewType) {
|
|
52
|
+
return explicit ?? readViewFromSearch(search) ?? metadataViewType;
|
|
53
|
+
}
|
|
54
|
+
export function DynamicView({ view, kanbanProps, ...tableProps }) {
|
|
55
|
+
const { model, endpoint, refreshTrigger, timeZone, currency } = tableProps;
|
|
56
|
+
const api = useApi();
|
|
57
|
+
const cached = useMetadataCache((s) => s.getMetadata(model));
|
|
58
|
+
const setMeta = useMetadataCache((s) => s.setMetadata);
|
|
59
|
+
const [viewType, setViewType] = useState(cached?.view_type);
|
|
60
|
+
const [resolved, setResolved] = useState(!!cached);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
const c = useMetadataCache.getState().getMetadata(model);
|
|
64
|
+
if (c) {
|
|
65
|
+
setViewType(c.view_type);
|
|
66
|
+
setResolved(true);
|
|
67
|
+
}
|
|
68
|
+
api
|
|
69
|
+
.get(`/metadata/table/${model}`)
|
|
70
|
+
.then((res) => {
|
|
71
|
+
if (cancelled)
|
|
72
|
+
return;
|
|
73
|
+
const body = res.data;
|
|
74
|
+
const meta = body?.success ? body.data : res.data;
|
|
75
|
+
if (meta) {
|
|
76
|
+
setViewType(meta.view_type);
|
|
77
|
+
setMeta(model, meta);
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {
|
|
81
|
+
/* fall back to the table renderer */
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
if (!cancelled)
|
|
85
|
+
setResolved(true);
|
|
86
|
+
});
|
|
87
|
+
return () => {
|
|
88
|
+
cancelled = true;
|
|
89
|
+
};
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [model]);
|
|
92
|
+
// The per-nav `view` (explicit prop or `?view=` query) wins over the
|
|
93
|
+
// model-level metadata default so two navs on the same model route to
|
|
94
|
+
// different surfaces.
|
|
95
|
+
const search = typeof window !== 'undefined' ? window.location.search : undefined;
|
|
96
|
+
const effectiveView = resolveActiveView(view, search, viewType);
|
|
97
|
+
// Until we know the view type, render nothing transient-heavy: default to the
|
|
98
|
+
// table renderer only once resolved to avoid a table→kanban flash. An
|
|
99
|
+
// explicit/query view short-circuits the wait (we already know the surface).
|
|
100
|
+
if (!resolved && !cached && view === undefined && !readViewFromSearch(search))
|
|
101
|
+
return null;
|
|
102
|
+
if (resolveViewRenderer(effectiveView) === 'kanban') {
|
|
103
|
+
return (_jsx(DynamicKanban, { model: model, endpoint: endpoint, refreshTrigger: refreshTrigger, timeZone: timeZone, currency: currency, ...kanbanProps }));
|
|
104
|
+
}
|
|
105
|
+
return _jsx(DynamicTable, { ...tableProps });
|
|
106
|
+
}
|
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, readViewFromSearch, resolveActiveView, 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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"}
|