@asteby/metacore-runtime-react 39.1.1 → 39.2.1
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/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/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 +32 -0
- package/dist/sidebar-layout-manager.d.ts.map +1 -0
- package/dist/sidebar-layout-manager.js +384 -0
- package/package.json +1 -1
- package/src/__tests__/apply-sidebar-layout.test.ts +74 -0
- package/src/apply-sidebar-layout.ts +290 -0
- package/src/index.ts +23 -0
- package/src/sidebar-layout-manager.tsx +833 -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/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'
|