@asteby/metacore-runtime-react 19.0.0 → 20.1.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 CHANGED
@@ -1,5 +1,48 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 20.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8de09a9: 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
+
9
+ ### Patch Changes
10
+
11
+ - 8de09a9: fix(kanban): el drag-to-move ya no duplica `/me` en el PUT (causaba 404 "No se pudo mover la tarjeta"); el board respeta el ancho del padre (`min-w-0`) y deja de desbordarse horizontalmente.
12
+
13
+ ## 20.0.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 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.
18
+ - 3f41073: Sidebar nav: exact, view-aware active-state so sibling navs over the same model light up one at a time
19
+
20
+ The `NavGroup` active-state matcher (`checkIsActive`) now treats `view`/`group_by`
21
+ query params as the _identity_ of a view-style nav item. Two navs over the same
22
+ model that differ only by their view — e.g. a "Board" (`?view=kanban&group_by=stage`)
23
+ and an "Issues" (`?view=list`, or a query-less default list) — are mutually
24
+ exclusive: only the item whose view identity equals `currentHref` stays active,
25
+ fixing the bug where both lit up at once.
26
+ - `@asteby/metacore-ui`: the matcher is extracted into a pure, React-free
27
+ `layout/nav-active` module (`checkIsActive`, `splitHref`, `declaredFiltersMatch`,
28
+ `VIEW_PARAMS`) and re-exported from `@asteby/metacore-ui/layout` for hosts and
29
+ unit tests. `f_` filter and transient (page/sort/search) highlight behaviour is
30
+ unchanged — a query-less link still highlights under filters/pagination, and
31
+ per-status entries still light up one at a time.
32
+ - `@asteby/metacore-starter-core`: the scaffold's `nav-group` matcher gains the
33
+ same view/query/filter-aware logic.
34
+ - `@asteby/metacore-runtime-react`: `DynamicView` now reads the active view from
35
+ the per-nav signal — an explicit `view` prop (host router) or the `?view=`
36
+ query — and prefers it over the model-level `metadata.view_type`, so the same
37
+ model can route `?view=kanban` to `DynamicKanban` and `?view=list` to
38
+ `DynamicTable` with no per-model metadata change. New pure helpers
39
+ `readViewFromSearch` / `resolveActiveView` are exported.
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [3f41073]
44
+ - @asteby/metacore-ui@2.6.0
45
+
3
46
  ## 19.0.0
4
47
 
5
48
  ### Major Changes
@@ -9,6 +52,7 @@
9
52
  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
53
 
11
54
  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`.
55
+
12
56
  ## 18.28.3
13
57
 
14
58
  ### Patch Changes
@@ -44,8 +44,8 @@ export interface DynamicKanbanProps {
44
44
  /** Model key as registered on the backend (e.g. "issue"). */
45
45
  model: string;
46
46
  /**
47
- * Data endpoint base. Defaults to `/data/<model>`. The optimistic update
48
- * PUTs to `<base>/me/<id>`.
47
+ * Data endpoint base the org-scoped LIST endpoint (e.g.
48
+ * `/data/<model>/me`). The optimistic update PUTs to `<base>/<id>`.
49
49
  */
50
50
  endpoint?: string;
51
51
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
@@ -1 +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"}
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,qBAmSpB"}
@@ -261,7 +261,11 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
261
261
  setRecords((rs) => rs.map((r) => String(r.id) === cardId ? { ...r, [groupByKey]: destStage } : r));
262
262
  try {
263
263
  const base = endpoint || `/data/${model}`;
264
- const res = (await api.put(`${base}/me/${cardId}`, {
264
+ // `base` is the org-scoped list endpoint (e.g. `/data/<model>/me`),
265
+ // so the per-record update is just `<base>/<id>` — same convention
266
+ // as DynamicTable/DynamicRelation. Appending an extra `/me` here
267
+ // produced `/data/<model>/me/me/<id>` → 404 on drag-to-move.
268
+ const res = (await api.put(`${base}/${cardId}`, {
265
269
  [groupByKey]: destStage,
266
270
  }));
267
271
  if (res?.data && res.data.success === false) {
@@ -298,7 +302,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
298
302
  order: Number.MAX_SAFE_INTEGER,
299
303
  });
300
304
  }
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) => {
305
+ return (_jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
302
306
  const cards = grouped.get(stage.key) ?? [];
303
307
  const droppableAllowed = !activeId ||
304
308
  stage.key === activeStage ||
@@ -313,6 +317,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, pa
313
317
  } }))] }));
314
318
  }
315
319
  function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
320
+ const { t } = useTranslation();
316
321
  const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
317
322
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
318
323
  isDark,
@@ -321,7 +326,7 @@ function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
321
326
  opacity: dimmed ? 0.45 : 1,
322
327
  outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
323
328
  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 }) })] }));
329
+ }, "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 }) })] }));
325
330
  }
326
331
  function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
327
332
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
@@ -5,7 +5,31 @@ import { type DynamicKanbanProps } from './dynamic-kanban';
5
5
  * host that resolves metadata itself can branch without mounting this wrapper.
6
6
  */
7
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;
8
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;
9
33
  /**
10
34
  * Props forwarded to <DynamicKanban> when the model resolves to a kanban
11
35
  * view. `model`/`endpoint`/`refreshTrigger`/`timeZone`/`currency` are shared
@@ -14,5 +38,5 @@ export interface DynamicViewProps extends DynamicTableProps {
14
38
  */
15
39
  kanbanProps?: Partial<Omit<DynamicKanbanProps, 'model' | 'endpoint'>>;
16
40
  }
17
- export declare function DynamicView({ kanbanProps, ...tableProps }: DynamicViewProps): import("react").JSX.Element | null;
41
+ export declare function DynamicView({ view, kanbanProps, ...tableProps }: DynamicViewProps): import("react").JSX.Element | null;
18
42
  //# sourceMappingURL=dynamic-view.d.ts.map
@@ -1 +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"}
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"}
@@ -26,7 +26,32 @@ import { DynamicKanban } from './dynamic-kanban';
26
26
  export function resolveViewRenderer(viewType) {
27
27
  return viewType === 'kanban' ? 'kanban' : 'table';
28
28
  }
29
- export function DynamicView({ kanbanProps, ...tableProps }) {
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 }) {
30
55
  const { model, endpoint, refreshTrigger, timeZone, currency } = tableProps;
31
56
  const api = useApi();
32
57
  const cached = useMetadataCache((s) => s.getMetadata(model));
@@ -64,11 +89,17 @@ export function DynamicView({ kanbanProps, ...tableProps }) {
64
89
  };
65
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
66
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);
67
97
  // 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)
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))
70
101
  return null;
71
- if (resolveViewRenderer(viewType) === 'kanban') {
102
+ if (resolveViewRenderer(effectiveView) === 'kanban') {
72
103
  return (_jsx(DynamicKanban, { model: model, endpoint: endpoint, refreshTrigger: refreshTrigger, timeZone: timeZone, currency: currency, ...kanbanProps }));
73
104
  }
74
105
  return _jsx(DynamicTable, { ...tableProps });
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from './types';
2
2
  export * from './options-context';
3
3
  export * from './dynamic-table';
4
4
  export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
5
- export { DynamicView, resolveViewRenderer, type DynamicViewProps, } from './dynamic-view';
5
+ export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
6
6
  export * from './dynamic-form';
7
7
  export { ActionModalDispatcher, type ActionModalProps, } from './action-modal-dispatcher';
8
8
  export { ModelActionToolbar, useModelActions, type ModelActionToolbarProps, type ActionPlacement, } from './model-action-toolbar';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export * from './types';
7
7
  export * from './options-context';
8
8
  export * from './dynamic-table';
9
9
  export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
10
- export { DynamicView, resolveViewRenderer, } from './dynamic-view';
10
+ export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, } from './dynamic-view';
11
11
  export * from './dynamic-form';
12
12
  export { ActionModalDispatcher, } from './action-modal-dispatcher';
13
13
  export { ModelActionToolbar, useModelActions, } from './model-action-toolbar';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "19.0.0",
3
+ "version": "20.1.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,8 @@
37
37
  "react-i18next": ">=13",
38
38
  "sonner": ">=1.7",
39
39
  "zustand": ">=5",
40
- "@asteby/metacore-ui": "^2.5.2",
41
- "@asteby/metacore-sdk": "^3.2.0"
40
+ "@asteby/metacore-sdk": "^3.2.0",
41
+ "@asteby/metacore-ui": "^2.6.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@tanstack/react-router": {
@@ -67,8 +67,8 @@
67
67
  "typescript": "^6.0.0",
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
- "@asteby/metacore-ui": "2.5.2",
71
- "@asteby/metacore-sdk": "3.2.0"
70
+ "@asteby/metacore-sdk": "3.2.0",
71
+ "@asteby/metacore-ui": "2.6.0"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,62 @@
1
+ // DynamicView routing decision: the same model exposes a "Board" nav
2
+ // (`?view=kanban`) and an "Issues" nav (`?view=list`), so the per-nav `view`
3
+ // signal — explicit prop or `?view=` query — must win over the model-level
4
+ // `metadata.view_type`. These are the pure helpers behind that decision.
5
+ import { describe, expect, it } from 'vitest'
6
+ import {
7
+ resolveViewRenderer,
8
+ readViewFromSearch,
9
+ resolveActiveView,
10
+ } from '../dynamic-view'
11
+
12
+ describe('resolveViewRenderer', () => {
13
+ it('maps kanban → kanban and everything else → table', () => {
14
+ expect(resolveViewRenderer('kanban')).toBe('kanban')
15
+ expect(resolveViewRenderer('list')).toBe('table')
16
+ expect(resolveViewRenderer('table')).toBe('table')
17
+ expect(resolveViewRenderer(undefined)).toBe('table')
18
+ })
19
+ })
20
+
21
+ describe('readViewFromSearch', () => {
22
+ it('reads view from a leading-? search string', () => {
23
+ expect(readViewFromSearch('?view=kanban&group_by=stage')).toBe('kanban')
24
+ })
25
+ it('reads view from a bare query string', () => {
26
+ expect(readViewFromSearch('view=list')).toBe('list')
27
+ })
28
+ it('reads view from a full href', () => {
29
+ expect(readViewFromSearch('/m/github_issues?view=kanban')).toBe('kanban')
30
+ })
31
+ it('returns undefined when absent or empty', () => {
32
+ expect(readViewFromSearch('?page=2')).toBeUndefined()
33
+ expect(readViewFromSearch('')).toBeUndefined()
34
+ expect(readViewFromSearch(undefined)).toBeUndefined()
35
+ })
36
+ })
37
+
38
+ describe('resolveActiveView precedence', () => {
39
+ it('explicit prop wins over query and metadata', () => {
40
+ expect(resolveActiveView('kanban', '?view=list', 'table')).toBe('kanban')
41
+ })
42
+ it('query wins over metadata when no explicit prop', () => {
43
+ expect(resolveActiveView(undefined, '?view=kanban', 'table')).toBe(
44
+ 'kanban',
45
+ )
46
+ })
47
+ it('falls back to metadata view_type when neither prop nor query present', () => {
48
+ expect(resolveActiveView(undefined, '?page=2', 'kanban')).toBe('kanban')
49
+ expect(resolveActiveView(undefined, undefined, 'table')).toBe('table')
50
+ })
51
+ it('Board vs Issues on the same model route to different surfaces', () => {
52
+ // model metadata default could be either; the nav query decides.
53
+ const boardView = resolveActiveView(
54
+ undefined,
55
+ '?view=kanban&group_by=stage',
56
+ 'list',
57
+ )
58
+ const issuesView = resolveActiveView(undefined, '?view=list', 'list')
59
+ expect(resolveViewRenderer(boardView)).toBe('kanban')
60
+ expect(resolveViewRenderer(issuesView)).toBe('table')
61
+ })
62
+ })
@@ -238,8 +238,8 @@ export interface DynamicKanbanProps {
238
238
  /** Model key as registered on the backend (e.g. "issue"). */
239
239
  model: string
240
240
  /**
241
- * Data endpoint base. Defaults to `/data/<model>`. The optimistic update
242
- * PUTs to `<base>/me/<id>`.
241
+ * Data endpoint base the org-scoped LIST endpoint (e.g.
242
+ * `/data/<model>/me`). The optimistic update PUTs to `<base>/<id>`.
243
243
  */
244
244
  endpoint?: string
245
245
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
@@ -411,7 +411,11 @@ export function DynamicKanban({
411
411
 
412
412
  try {
413
413
  const base = endpoint || `/data/${model}`
414
- const res = (await api.put(`${base}/me/${cardId}`, {
414
+ // `base` is the org-scoped list endpoint (e.g. `/data/<model>/me`),
415
+ // so the per-record update is just `<base>/<id>` — same convention
416
+ // as DynamicTable/DynamicRelation. Appending an extra `/me` here
417
+ // produced `/data/<model>/me/me/<id>` → 404 on drag-to-move.
418
+ const res = (await api.put(`${base}/${cardId}`, {
415
419
  [groupByKey]: destStage,
416
420
  })) as { data?: ApiResponse<any> }
417
421
  if (res?.data && res.data.success === false) {
@@ -473,7 +477,7 @@ export function DynamicKanban({
473
477
 
474
478
  return (
475
479
  <DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd}>
476
- <div className="flex gap-4 overflow-x-auto p-1" data-testid="kanban-board">
480
+ <div className="flex min-w-0 gap-4 overflow-x-auto p-1" data-testid="kanban-board">
477
481
  {lanes.map((stage) => {
478
482
  const cards = grouped.get(stage.key) ?? []
479
483
  const droppableAllowed =
@@ -568,6 +572,7 @@ interface KanbanLaneProps {
568
572
  }
569
573
 
570
574
  function KanbanLane({ stage, count, isDark, dimmed, disabled, children }: KanbanLaneProps) {
575
+ const { t } = useTranslation()
571
576
  const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled })
572
577
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
573
578
  isDark,
@@ -590,13 +595,13 @@ function KanbanLane({ stage, count, isDark, dimmed, disabled, children }: Kanban
590
595
  className="border-0 text-xs font-semibold"
591
596
  style={headerStyle}
592
597
  >
593
- {stage.label}
598
+ {t(stage.label, { defaultValue: stage.label })}
594
599
  </Badge>
595
600
  <span className="text-xs font-medium tabular-nums text-muted-foreground">
596
601
  {count}
597
602
  </span>
598
603
  </div>
599
- <ScrollArea className="max-h-[70vh]">
604
+ <ScrollArea className="min-h-[55vh] max-h-[70vh]">
600
605
  <div className="flex flex-col gap-2 px-2 pb-3">{children}</div>
601
606
  </ScrollArea>
602
607
  </div>
@@ -30,7 +30,45 @@ export function resolveViewRenderer(
30
30
  return viewType === 'kanban' ? 'kanban' : 'table'
31
31
  }
32
32
 
33
+ /**
34
+ * Reads the `view` selector out of a URL search string (`?view=kanban`, a bare
35
+ * `view=kanban`, or a full href). Returns `undefined` when absent. The query is
36
+ * the per-NAV signal: the same model exposes a "Board" nav (`?view=kanban`) and
37
+ * an "Issues" nav (`?view=list`), so the query — not the model-level
38
+ * `metadata.view_type` — decides which surface to paint. SSR-safe.
39
+ */
40
+ export function readViewFromSearch(search?: string): string | undefined {
41
+ if (!search) return undefined
42
+ const qIndex = search.indexOf('?')
43
+ const qs = qIndex === -1 ? search : search.slice(qIndex + 1)
44
+ const v = new URLSearchParams(qs).get('view')
45
+ return v ?? undefined
46
+ }
47
+
48
+ /**
49
+ * Resolves the effective view selector with the right precedence:
50
+ * 1. an explicit `view` prop the host passes (it owns the router), then
51
+ * 2. the `view` query param, then
52
+ * 3. the model-level `metadata.view_type` default.
53
+ * Exported pure for unit tests and host reuse.
54
+ */
55
+ export function resolveActiveView(
56
+ explicit: string | undefined,
57
+ search: string | undefined,
58
+ metadataViewType: string | undefined,
59
+ ): string | undefined {
60
+ return explicit ?? readViewFromSearch(search) ?? metadataViewType
61
+ }
62
+
33
63
  export interface DynamicViewProps extends DynamicTableProps {
64
+ /**
65
+ * Explicit view selector from the host's router (e.g. the `view` search
66
+ * param resolved by tanstack-router). Takes precedence over the query string
67
+ * and the model's `metadata.view_type`. Pass this when the host owns routing
68
+ * so the same model can show `?view=kanban` (board) or `?view=list` (table)
69
+ * with no per-model metadata change.
70
+ */
71
+ view?: string
34
72
  /**
35
73
  * Props forwarded to <DynamicKanban> when the model resolves to a kanban
36
74
  * view. `model`/`endpoint`/`refreshTrigger`/`timeZone`/`currency` are shared
@@ -40,7 +78,7 @@ export interface DynamicViewProps extends DynamicTableProps {
40
78
  kanbanProps?: Partial<Omit<DynamicKanbanProps, 'model' | 'endpoint'>>
41
79
  }
42
80
 
43
- export function DynamicView({ kanbanProps, ...tableProps }: DynamicViewProps) {
81
+ export function DynamicView({ view, kanbanProps, ...tableProps }: DynamicViewProps) {
44
82
  const { model, endpoint, refreshTrigger, timeZone, currency } = tableProps
45
83
  const api = useApi()
46
84
  const cached = useMetadataCache((s) => s.getMetadata(model))
@@ -78,11 +116,20 @@ export function DynamicView({ kanbanProps, ...tableProps }: DynamicViewProps) {
78
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
79
117
  }, [model])
80
118
 
119
+ // The per-nav `view` (explicit prop or `?view=` query) wins over the
120
+ // model-level metadata default so two navs on the same model route to
121
+ // different surfaces.
122
+ const search =
123
+ typeof window !== 'undefined' ? window.location.search : undefined
124
+ const effectiveView = resolveActiveView(view, search, viewType)
125
+
81
126
  // Until we know the view type, render nothing transient-heavy: default to the
82
- // table renderer only once resolved to avoid a table→kanban flash.
83
- if (!resolved && !cached) return null
127
+ // table renderer only once resolved to avoid a table→kanban flash. An
128
+ // explicit/query view short-circuits the wait (we already know the surface).
129
+ if (!resolved && !cached && view === undefined && !readViewFromSearch(search))
130
+ return null
84
131
 
85
- if (resolveViewRenderer(viewType) === 'kanban') {
132
+ if (resolveViewRenderer(effectiveView) === 'kanban') {
86
133
  return (
87
134
  <DynamicKanban
88
135
  model={model}
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export {
19
19
  export {
20
20
  DynamicView,
21
21
  resolveViewRenderer,
22
+ readViewFromSearch,
23
+ resolveActiveView,
22
24
  type DynamicViewProps,
23
25
  } from './dynamic-view'
24
26
  export * from './dynamic-form'