@asteby/metacore-runtime-react 39.1.0 → 39.2.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 +12 -0
- package/LICENSE +201 -0
- package/dist/apply-sidebar-layout.d.ts +86 -0
- package/dist/apply-sidebar-layout.d.ts.map +1 -0
- package/dist/apply-sidebar-layout.js +226 -0
- package/dist/dynamic-form-schema.d.ts +1 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +32 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sidebar-layout-manager.d.ts +36 -0
- package/dist/sidebar-layout-manager.d.ts.map +1 -0
- package/dist/sidebar-layout-manager.js +204 -0
- package/package.json +7 -7
- package/src/__tests__/apply-sidebar-layout.test.ts +74 -0
- package/src/__tests__/dynamic-table-forbidden.test.tsx +67 -0
- package/src/apply-sidebar-layout.ts +290 -0
- package/src/dynamic-table.tsx +51 -6
- package/src/index.ts +23 -0
- package/src/sidebar-layout-manager.tsx +522 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply-sidebar-layout — pure overlay of an org sidebar layout onto a live
|
|
3
|
+
* catalog of leaves. Mirrors ops `services.ApplySidebarLayout` (Go). See
|
|
4
|
+
* CONTRACT-ORG-SIDEBAR-LAYOUT.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SidebarNodeType = 'group' | 'folder' | 'leaf'
|
|
8
|
+
|
|
9
|
+
export interface SidebarLayoutNode {
|
|
10
|
+
type: SidebarNodeType
|
|
11
|
+
key?: string
|
|
12
|
+
title?: string
|
|
13
|
+
icon?: string
|
|
14
|
+
ref?: string
|
|
15
|
+
hidden?: boolean
|
|
16
|
+
children?: SidebarLayoutNode[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SidebarLayoutDoc {
|
|
20
|
+
version?: number
|
|
21
|
+
tree: SidebarLayoutNode[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CatalogLeaf {
|
|
25
|
+
ref: string
|
|
26
|
+
title: string
|
|
27
|
+
url: string
|
|
28
|
+
icon?: string
|
|
29
|
+
/** Optional alternate refs that resolve to the same leaf. */
|
|
30
|
+
aliases?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AppliedNavItem {
|
|
34
|
+
title: string
|
|
35
|
+
url: string
|
|
36
|
+
icon?: string
|
|
37
|
+
ref?: string
|
|
38
|
+
folderKey?: string
|
|
39
|
+
items?: AppliedNavItem[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AppliedNavGroup {
|
|
43
|
+
title: string
|
|
44
|
+
icon?: string
|
|
45
|
+
items: AppliedNavItem[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Normalize a nav URL/path for stable refs. */
|
|
49
|
+
export function normalizeNavPath(raw: string): string {
|
|
50
|
+
const trimmed = (raw || '').trim()
|
|
51
|
+
if (!trimmed) return '/'
|
|
52
|
+
try {
|
|
53
|
+
const u = new URL(trimmed, 'http://local.invalid')
|
|
54
|
+
let path = u.pathname || '/'
|
|
55
|
+
if (path !== '/') path = path.replace(/\/+$/, '')
|
|
56
|
+
return u.search ? `${path}${u.search}` : path
|
|
57
|
+
} catch {
|
|
58
|
+
return trimmed.replace(/\/+$/, '') || '/'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function coreRefKey(url: string): string {
|
|
63
|
+
return `core:${normalizeNavPath(url)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function addonModelRefKey(addonKey: string, table: string, query = ''): string {
|
|
67
|
+
let ref = `addon:${addonKey.trim()}/model:${table.trim().toLowerCase()}`
|
|
68
|
+
const q = query.replace(/^\?/, '').trim()
|
|
69
|
+
if (q) ref += `?${q}`
|
|
70
|
+
return ref
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function addonUrlRefKey(addonKey: string, path: string): string {
|
|
74
|
+
return `addon:${addonKey.trim()}/url:${normalizeNavPath(path)}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function urlRefKey(url: string): string {
|
|
78
|
+
return `url:${normalizeNavPath(url)}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function folderAccessCapability(folderKey: string): string {
|
|
82
|
+
return `folder.${folderKey}.access`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pick the best stable ref for a live nav leaf.
|
|
87
|
+
* Prefer addon-aware forms when metadata is known; fall back to url:/core:.
|
|
88
|
+
*/
|
|
89
|
+
export function leafRefFor(
|
|
90
|
+
url: string,
|
|
91
|
+
meta?: { addonKey?: string; model?: string },
|
|
92
|
+
): string {
|
|
93
|
+
const path = normalizeNavPath(url)
|
|
94
|
+
if (meta?.addonKey && meta?.model) {
|
|
95
|
+
const q = path.includes('?') ? path.slice(path.indexOf('?') + 1) : ''
|
|
96
|
+
return addonModelRefKey(meta.addonKey, meta.model, q)
|
|
97
|
+
}
|
|
98
|
+
const addonMatch = /^\/addons\/([^/]+)(?:\/(.*))?$/.exec(path.split('?')[0] ?? '')
|
|
99
|
+
if (addonMatch) {
|
|
100
|
+
const rest = addonMatch[2] ? `/${addonMatch[2]}` : '/'
|
|
101
|
+
const q = path.includes('?') ? path.slice(path.indexOf('?')) : ''
|
|
102
|
+
return addonUrlRefKey(addonMatch[1], rest + q)
|
|
103
|
+
}
|
|
104
|
+
if (path.startsWith('/m/')) return urlRefKey(path)
|
|
105
|
+
return coreRefKey(path)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function indexCatalog(catalog: CatalogLeaf[]): Map<string, CatalogLeaf> {
|
|
109
|
+
const map = new Map<string, CatalogLeaf>()
|
|
110
|
+
for (const leaf of catalog) {
|
|
111
|
+
if (!leaf.ref) continue
|
|
112
|
+
map.set(leaf.ref, leaf)
|
|
113
|
+
for (const a of leaf.aliases ?? []) map.set(a, leaf)
|
|
114
|
+
// Always index by url: form so layouts saved with either scheme resolve.
|
|
115
|
+
map.set(urlRefKey(leaf.url), leaf)
|
|
116
|
+
if (!leaf.url.startsWith('/m/') && !leaf.url.startsWith('/addons/')) {
|
|
117
|
+
map.set(coreRefKey(leaf.url), leaf)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return map
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Apply an org layout onto a catalog. Returns `null` when the tree is empty
|
|
125
|
+
* (caller keeps the default merge).
|
|
126
|
+
*/
|
|
127
|
+
export function applySidebarLayout(
|
|
128
|
+
catalog: CatalogLeaf[],
|
|
129
|
+
tree: SidebarLayoutNode[] | undefined | null,
|
|
130
|
+
): AppliedNavGroup[] | null {
|
|
131
|
+
if (!tree || tree.length === 0) return null
|
|
132
|
+
const byRef = indexCatalog(catalog)
|
|
133
|
+
const groups: AppliedNavGroup[] = []
|
|
134
|
+
|
|
135
|
+
for (const n of tree) {
|
|
136
|
+
if (n.hidden) continue
|
|
137
|
+
switch (n.type) {
|
|
138
|
+
case 'group': {
|
|
139
|
+
const items = applyChildren(n.children ?? [], byRef)
|
|
140
|
+
if (items.length === 0) break
|
|
141
|
+
groups.push({ title: n.title || n.key || '', icon: n.icon, items })
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
case 'folder': {
|
|
145
|
+
const item = applyFolder(n, byRef)
|
|
146
|
+
if (!item) break
|
|
147
|
+
groups.push({ title: n.title || n.key || '', icon: n.icon, items: [item] })
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
case 'leaf': {
|
|
151
|
+
const item = applyLeaf(n, byRef)
|
|
152
|
+
if (item) groups.push({ title: 'sidebar.general', items: [item] })
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return groups.length > 0 ? groups : null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyChildren(
|
|
161
|
+
nodes: SidebarLayoutNode[],
|
|
162
|
+
byRef: Map<string, CatalogLeaf>,
|
|
163
|
+
): AppliedNavItem[] {
|
|
164
|
+
const out: AppliedNavItem[] = []
|
|
165
|
+
for (const n of nodes) {
|
|
166
|
+
if (n.hidden) continue
|
|
167
|
+
if (n.type === 'folder') {
|
|
168
|
+
const item = applyFolder(n, byRef)
|
|
169
|
+
if (item) out.push(item)
|
|
170
|
+
} else if (n.type === 'leaf') {
|
|
171
|
+
const item = applyLeaf(n, byRef)
|
|
172
|
+
if (item) out.push(item)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function applyFolder(
|
|
179
|
+
n: SidebarLayoutNode,
|
|
180
|
+
byRef: Map<string, CatalogLeaf>,
|
|
181
|
+
): AppliedNavItem | null {
|
|
182
|
+
const children = applyChildren(n.children ?? [], byRef)
|
|
183
|
+
if (children.length === 0) return null
|
|
184
|
+
return {
|
|
185
|
+
title: n.title || n.key || '',
|
|
186
|
+
url: children[0]?.url ?? '#',
|
|
187
|
+
icon: n.icon,
|
|
188
|
+
folderKey: n.key,
|
|
189
|
+
items: children,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyLeaf(
|
|
194
|
+
n: SidebarLayoutNode,
|
|
195
|
+
byRef: Map<string, CatalogLeaf>,
|
|
196
|
+
): AppliedNavItem | null {
|
|
197
|
+
if (!n.ref) return null
|
|
198
|
+
const leaf = byRef.get(n.ref)
|
|
199
|
+
if (!leaf) return null
|
|
200
|
+
return {
|
|
201
|
+
title: n.title?.trim() ? n.title : leaf.title,
|
|
202
|
+
url: leaf.url,
|
|
203
|
+
icon: n.icon || leaf.icon,
|
|
204
|
+
ref: n.ref,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Collect folder keys from a tree (for permissions catalog). */
|
|
209
|
+
export function collectFolderKeys(tree: SidebarLayoutNode[]): string[] {
|
|
210
|
+
const out: string[] = []
|
|
211
|
+
const seen = new Set<string>()
|
|
212
|
+
const walk = (nodes: SidebarLayoutNode[]) => {
|
|
213
|
+
for (const n of nodes) {
|
|
214
|
+
if (n.type === 'folder' && n.key && !seen.has(n.key)) {
|
|
215
|
+
seen.add(n.key)
|
|
216
|
+
out.push(n.key)
|
|
217
|
+
}
|
|
218
|
+
if (n.children?.length) walk(n.children)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
walk(tree)
|
|
222
|
+
return out
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const FOLDER_KEY_RE = /^[a-z][a-z0-9_]{0,39}$/
|
|
226
|
+
|
|
227
|
+
export function slugifyFolderKey(title: string): string {
|
|
228
|
+
let s = title
|
|
229
|
+
.toLowerCase()
|
|
230
|
+
.trim()
|
|
231
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
232
|
+
.replace(/^_+|_+$/g, '')
|
|
233
|
+
if (!s) s = 'folder'
|
|
234
|
+
if (s.length > 40) s = s.slice(0, 40)
|
|
235
|
+
if (!/^[a-z]/.test(s)) s = `f_${s}`.slice(0, 40)
|
|
236
|
+
return s
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function isValidFolderKey(key: string): boolean {
|
|
240
|
+
return FOLDER_KEY_RE.test(key)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build a default layout tree from the live nav groups (one group node per
|
|
245
|
+
* section, leaves + existing collapsibles as folders). Used by the editor
|
|
246
|
+
* "start from current sidebar" action.
|
|
247
|
+
*/
|
|
248
|
+
export function layoutTreeFromNavGroups(
|
|
249
|
+
groups: Array<{
|
|
250
|
+
title: string
|
|
251
|
+
items: Array<{
|
|
252
|
+
title: string
|
|
253
|
+
url: string
|
|
254
|
+
icon?: string
|
|
255
|
+
items?: Array<{ title: string; url: string; icon?: string; items?: unknown[] }>
|
|
256
|
+
folderKey?: string
|
|
257
|
+
ref?: string
|
|
258
|
+
}>
|
|
259
|
+
}>,
|
|
260
|
+
refFor: (url: string) => string = (u) => leafRefFor(u),
|
|
261
|
+
): SidebarLayoutNode[] {
|
|
262
|
+
return groups.map((g) => ({
|
|
263
|
+
type: 'group' as const,
|
|
264
|
+
key: g.title,
|
|
265
|
+
title: g.title,
|
|
266
|
+
children: g.items.map((it) => {
|
|
267
|
+
if (it.items && it.items.length > 0) {
|
|
268
|
+
const key = it.folderKey || slugifyFolderKey(it.title)
|
|
269
|
+
return {
|
|
270
|
+
type: 'folder' as const,
|
|
271
|
+
key,
|
|
272
|
+
title: it.title,
|
|
273
|
+
icon: typeof it.icon === 'string' ? it.icon : undefined,
|
|
274
|
+
children: it.items.map((child) => ({
|
|
275
|
+
type: 'leaf' as const,
|
|
276
|
+
ref: (child as { ref?: string }).ref || refFor(child.url),
|
|
277
|
+
title: child.title,
|
|
278
|
+
icon: typeof child.icon === 'string' ? child.icon : undefined,
|
|
279
|
+
})),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
type: 'leaf' as const,
|
|
284
|
+
ref: it.ref || refFor(it.url),
|
|
285
|
+
title: it.title,
|
|
286
|
+
icon: typeof it.icon === 'string' ? it.icon : undefined,
|
|
287
|
+
}
|
|
288
|
+
}),
|
|
289
|
+
}))
|
|
290
|
+
}
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -260,6 +260,12 @@ export interface DynamicTableProps {
|
|
|
260
260
|
virtualizeRows?: boolean | number
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** True when an api-client error is an HTTP 403 (axios-style or fetch-style). */
|
|
264
|
+
function isForbiddenError(error: unknown): boolean {
|
|
265
|
+
const e = error as { response?: { status?: number }; status?: number } | null
|
|
266
|
+
return e?.response?.status === 403 || e?.status === 403
|
|
267
|
+
}
|
|
268
|
+
|
|
263
269
|
export function DynamicTable({
|
|
264
270
|
model,
|
|
265
271
|
endpoint,
|
|
@@ -322,6 +328,10 @@ export function DynamicTable({
|
|
|
322
328
|
// even if meta.total says otherwise (count/list drift would otherwise
|
|
323
329
|
// keep the sentinel re-firing forever). Reset by any page-1 fetch.
|
|
324
330
|
const [infExhausted, setInfExhausted] = useState(false)
|
|
331
|
+
// The list endpoint answered 403: the user lacks read permission on this
|
|
332
|
+
// model. Rendered as "no permission" instead of masquerading as an empty
|
|
333
|
+
// list. Purely display state — never a fetch dependency (no refetch loop).
|
|
334
|
+
const [forbidden, setForbidden] = useState(false)
|
|
325
335
|
const infPageRef = useRef(1)
|
|
326
336
|
const [optionsMap, setOptionsMap] = useState<Map<string, any[]>>(new Map())
|
|
327
337
|
|
|
@@ -859,6 +869,7 @@ export function DynamicTable({
|
|
|
859
869
|
...buildFilterParams(),
|
|
860
870
|
}
|
|
861
871
|
const res = await api.get(endpoint || `/data/${model}`, { params }) as { data: ApiResponse<any[]> }
|
|
872
|
+
setForbidden(false)
|
|
862
873
|
if (res.data.success) {
|
|
863
874
|
const rows = res.data.data || []
|
|
864
875
|
setData(rows)
|
|
@@ -874,7 +885,13 @@ export function DynamicTable({
|
|
|
874
885
|
}
|
|
875
886
|
}
|
|
876
887
|
} catch (error) {
|
|
877
|
-
|
|
888
|
+
if (isForbiddenError(error)) {
|
|
889
|
+
setForbidden(true)
|
|
890
|
+
setData([])
|
|
891
|
+
setRowCount(0)
|
|
892
|
+
} else {
|
|
893
|
+
console.error('Error al cargar los datos', error)
|
|
894
|
+
}
|
|
878
895
|
} finally {
|
|
879
896
|
setLoadingData(false)
|
|
880
897
|
}
|
|
@@ -922,6 +939,7 @@ export function DynamicTable({
|
|
|
922
939
|
const res = (await api.get(endpoint || `/data/${model}`, {
|
|
923
940
|
params,
|
|
924
941
|
})) as { data: ApiResponse<any[]> }
|
|
942
|
+
setForbidden(false)
|
|
925
943
|
if (res.data.success) {
|
|
926
944
|
const rows = res.data.data || []
|
|
927
945
|
setData((prev) => (append ? dedupeById(prev, rows) : rows))
|
|
@@ -941,7 +959,16 @@ export function DynamicTable({
|
|
|
941
959
|
setInfExhausted(rows.length < infPageSize)
|
|
942
960
|
}
|
|
943
961
|
} catch (error) {
|
|
944
|
-
|
|
962
|
+
if (isForbiddenError(error)) {
|
|
963
|
+
setForbidden(true)
|
|
964
|
+
if (!append) {
|
|
965
|
+
setData([])
|
|
966
|
+
setRowCount(0)
|
|
967
|
+
}
|
|
968
|
+
setInfExhausted(true)
|
|
969
|
+
} else {
|
|
970
|
+
console.error('Error al cargar los datos', error)
|
|
971
|
+
}
|
|
945
972
|
} finally {
|
|
946
973
|
if (append) setLoadingMore(false)
|
|
947
974
|
else setLoadingData(false)
|
|
@@ -1546,8 +1573,17 @@ export function DynamicTable({
|
|
|
1546
1573
|
<Inbox className="h-10 w-10" />
|
|
1547
1574
|
</div>
|
|
1548
1575
|
<div className="flex flex-col items-center gap-1">
|
|
1549
|
-
|
|
1550
|
-
|
|
1576
|
+
{forbidden ? (
|
|
1577
|
+
<>
|
|
1578
|
+
<h3 className="text-lg font-semibold text-foreground" role="alert">{t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' })}</h3>
|
|
1579
|
+
<p className="text-sm text-muted-foreground">{t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' })}</p>
|
|
1580
|
+
</>
|
|
1581
|
+
) : (
|
|
1582
|
+
<>
|
|
1583
|
+
<h3 className="text-lg font-semibold text-foreground">No se encontraron resultados</h3>
|
|
1584
|
+
<p className="text-sm text-muted-foreground">No hay datos para mostrar en este momento.</p>
|
|
1585
|
+
</>
|
|
1586
|
+
)}
|
|
1551
1587
|
</div>
|
|
1552
1588
|
</div>
|
|
1553
1589
|
</TableCell>
|
|
@@ -1736,8 +1772,17 @@ export function DynamicTable({
|
|
|
1736
1772
|
<div className='flex h-16 w-16 items-center justify-center rounded-full bg-muted/50'>
|
|
1737
1773
|
<Inbox className='h-8 w-8' />
|
|
1738
1774
|
</div>
|
|
1739
|
-
|
|
1740
|
-
|
|
1775
|
+
{forbidden ? (
|
|
1776
|
+
<>
|
|
1777
|
+
<h3 className='text-base font-semibold text-foreground' role='alert'>{t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' })}</h3>
|
|
1778
|
+
<p className='text-sm text-muted-foreground'>{t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' })}</p>
|
|
1779
|
+
</>
|
|
1780
|
+
) : (
|
|
1781
|
+
<>
|
|
1782
|
+
<h3 className='text-base font-semibold text-foreground'>No se encontraron resultados</h3>
|
|
1783
|
+
<p className='text-sm text-muted-foreground'>No hay datos para mostrar en este momento.</p>
|
|
1784
|
+
</>
|
|
1785
|
+
)}
|
|
1741
1786
|
</div>
|
|
1742
1787
|
)}
|
|
1743
1788
|
{infiniteScroll && (
|
package/src/index.ts
CHANGED
|
@@ -216,6 +216,29 @@ export {
|
|
|
216
216
|
type RoleDef,
|
|
217
217
|
type RoleInput,
|
|
218
218
|
} from './permissions-manager'
|
|
219
|
+
export {
|
|
220
|
+
applySidebarLayout,
|
|
221
|
+
collectFolderKeys,
|
|
222
|
+
folderAccessCapability,
|
|
223
|
+
coreRefKey,
|
|
224
|
+
addonModelRefKey,
|
|
225
|
+
addonUrlRefKey,
|
|
226
|
+
urlRefKey,
|
|
227
|
+
leafRefFor,
|
|
228
|
+
normalizeNavPath,
|
|
229
|
+
slugifyFolderKey,
|
|
230
|
+
isValidFolderKey,
|
|
231
|
+
layoutTreeFromNavGroups,
|
|
232
|
+
type CatalogLeaf,
|
|
233
|
+
type SidebarLayoutDoc,
|
|
234
|
+
type SidebarLayoutNode,
|
|
235
|
+
type AppliedNavGroup,
|
|
236
|
+
type AppliedNavItem,
|
|
237
|
+
} from './apply-sidebar-layout'
|
|
238
|
+
export {
|
|
239
|
+
SidebarLayoutManager,
|
|
240
|
+
type SidebarLayoutManagerProps,
|
|
241
|
+
} from './sidebar-layout-manager'
|
|
219
242
|
export * from './org-runtime-context'
|
|
220
243
|
export * from './org-runtime-provider'
|
|
221
244
|
export * from './navigation-builder'
|