@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
|
@@ -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
|
/**
|