@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.
@@ -0,0 +1,146 @@
1
+ // DynamicView — the single entry point a host renders for a model's "list"
2
+ // surface. It picks the renderer from the model's `view_type`:
3
+ // - `'kanban'` → <DynamicKanban>
4
+ // - anything else / absent → <DynamicTable> (the default)
5
+ //
6
+ // The decision is metadata-driven (RFC §1.2): the kernel serves `view_type` +
7
+ // `group_by` on the table metadata, derived from the nav item. A host that
8
+ // already knows the view type can skip this and render the concrete component
9
+ // directly; a generic host route (e.g. ops `/m/$model`) mounts <DynamicView>
10
+ // and lets the metadata decide, so the same model can expose a `table` nav and
11
+ // a `kanban` nav with no host code change.
12
+ //
13
+ // Both child components fetch their own metadata (cache-backed), so the extra
14
+ // read this wrapper does to learn `view_type` is served from the same cache —
15
+ // no duplicate network round-trip in practice.
16
+ import { useEffect, useState } from 'react'
17
+ import { useApi } from './api-context'
18
+ import { useMetadataCache } from './metadata-cache'
19
+ import { DynamicTable, type DynamicTableProps } from './dynamic-table'
20
+ import { DynamicKanban, type DynamicKanbanProps } from './dynamic-kanban'
21
+ import type { TableMetadata, ApiResponse } from './types'
22
+
23
+ /**
24
+ * Pure routing decision: which renderer a `view_type` maps onto. Exported so a
25
+ * host that resolves metadata itself can branch without mounting this wrapper.
26
+ */
27
+ export function resolveViewRenderer(
28
+ viewType: string | undefined,
29
+ ): 'kanban' | 'table' {
30
+ return viewType === 'kanban' ? 'kanban' : 'table'
31
+ }
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
+
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
72
+ /**
73
+ * Props forwarded to <DynamicKanban> when the model resolves to a kanban
74
+ * view. `model`/`endpoint`/`refreshTrigger`/`timeZone`/`currency` are shared
75
+ * with the table props and forwarded automatically; this is for the
76
+ * kanban-only extras (e.g. `onCardClick`, `pageSize`).
77
+ */
78
+ kanbanProps?: Partial<Omit<DynamicKanbanProps, 'model' | 'endpoint'>>
79
+ }
80
+
81
+ export function DynamicView({ view, kanbanProps, ...tableProps }: DynamicViewProps) {
82
+ const { model, endpoint, refreshTrigger, timeZone, currency } = tableProps
83
+ const api = useApi()
84
+ const cached = useMetadataCache((s) => s.getMetadata(model))
85
+ const setMeta = useMetadataCache((s) => s.setMetadata)
86
+ const [viewType, setViewType] = useState<string | undefined>(cached?.view_type)
87
+ const [resolved, setResolved] = useState<boolean>(!!cached)
88
+
89
+ useEffect(() => {
90
+ let cancelled = false
91
+ const c = useMetadataCache.getState().getMetadata(model)
92
+ if (c) {
93
+ setViewType(c.view_type)
94
+ setResolved(true)
95
+ }
96
+ api
97
+ .get(`/metadata/table/${model}`)
98
+ .then((res) => {
99
+ if (cancelled) return
100
+ const body = res.data as ApiResponse<TableMetadata>
101
+ const meta = body?.success ? body.data : (res.data as TableMetadata)
102
+ if (meta) {
103
+ setViewType(meta.view_type)
104
+ setMeta(model, meta)
105
+ }
106
+ })
107
+ .catch(() => {
108
+ /* fall back to the table renderer */
109
+ })
110
+ .finally(() => {
111
+ if (!cancelled) setResolved(true)
112
+ })
113
+ return () => {
114
+ cancelled = true
115
+ }
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
117
+ }, [model])
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
+
126
+ // Until we know the view type, render nothing transient-heavy: default to the
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
131
+
132
+ if (resolveViewRenderer(effectiveView) === 'kanban') {
133
+ return (
134
+ <DynamicKanban
135
+ model={model}
136
+ endpoint={endpoint}
137
+ refreshTrigger={refreshTrigger}
138
+ timeZone={timeZone}
139
+ currency={currency}
140
+ {...kanbanProps}
141
+ />
142
+ )
143
+ }
144
+
145
+ return <DynamicTable {...tableProps} />
146
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,23 @@
6
6
  export * from './types'
7
7
  export * from './options-context'
8
8
  export * from './dynamic-table'
9
+ export {
10
+ DynamicKanban,
11
+ type DynamicKanbanProps,
12
+ deriveStages,
13
+ groupByStage,
14
+ isTransitionAllowed,
15
+ applyOptimisticMove,
16
+ selectCardColumns,
17
+ UNASSIGNED_LANE,
18
+ } from './dynamic-kanban'
19
+ export {
20
+ DynamicView,
21
+ resolveViewRenderer,
22
+ readViewFromSearch,
23
+ resolveActiveView,
24
+ type DynamicViewProps,
25
+ } from './dynamic-view'
9
26
  export * from './dynamic-form'
10
27
  export {
11
28
  ActionModalDispatcher,
package/src/types.ts CHANGED
@@ -22,6 +22,54 @@ export interface TableMetadata {
22
22
  * and attachments. Absent on hosts/older kernels — purely additive.
23
23
  */
24
24
  relations?: RelationMeta[]
25
+ /**
26
+ * Which renderer the host should use for this view. `'table'` (default, or
27
+ * absent) → `DynamicTable`; `'kanban'` → `DynamicKanban`. Served by the
28
+ * kernel from the nav item's `view_type` (RFC §1.2). Purely additive — older
29
+ * kernels omit it and the SDK falls back to the table renderer.
30
+ */
31
+ view_type?: 'table' | 'kanban' | (string & {})
32
+ /**
33
+ * Column key the board groups by when `view_type === 'kanban'` (the stage
34
+ * column, e.g. `'stage'`). Each distinct value of this column becomes a board
35
+ * lane. Mirrors the nav item's `group_by` (RFC §1.2).
36
+ */
37
+ group_by?: string
38
+ /**
39
+ * Board lanes (the stage machine of the `group_by`/`stage_field` column).
40
+ * When present the kanban renders one lane per stage in `order`. When absent
41
+ * the SDK derives lanes from the `group_by` column's `options` (the kernel
42
+ * already projects `stages[]` onto the status display — RFC §1.1). Snake_case
43
+ * keys as the kernel serves them.
44
+ */
45
+ stages?: StageMeta[]
46
+ /**
47
+ * Allowed stage transitions (RFC §1.1). When present, the kanban only lets a
48
+ * card drop into a lane reachable from its current stage; disallowed lanes
49
+ * are dimmed and reject the drop. `from`/`to` accept `'*'` as a wildcard.
50
+ * Absent → any move is allowed (the kernel still validates server-side).
51
+ */
52
+ transitions?: StageTransition[]
53
+ }
54
+
55
+ /**
56
+ * One board lane / pipeline stage. Mirrors the kernel v3 `Stage` (RFC §1.1).
57
+ * `color` is a semantic palette name (`'slate'`, `'blue'`, `'amber'`, `'green'`)
58
+ * or a hex literal — resolved through the same `generateBadgeStyles` helper as
59
+ * option badges. `is_final` flags a terminal stage (e.g. "Done").
60
+ */
61
+ export interface StageMeta {
62
+ key: string
63
+ label: string
64
+ color?: string
65
+ order?: number
66
+ is_final?: boolean
67
+ }
68
+
69
+ /** Allowed `from → to` stage transition (RFC §1.1). `'*'` is a wildcard. */
70
+ export interface StageTransition {
71
+ from: string
72
+ to: string
25
73
  }
26
74
 
27
75
  /**