@open-mercato/ui 0.4.11-develop.2631.481e9df5b0 → 0.5.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/.turbo/turbo-build.log +2 -2
- package/AGENTS.md +28 -4
- package/agentic/standalone-guide.md +97 -0
- package/build.mjs +10 -6
- package/dist/backend/AppShell.js +15 -2
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/DataTable.js +22 -1
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +1 -5
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +2 -5
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +2 -6
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +93 -3
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/markdown/MarkdownContent.js +47 -4
- package/dist/backend/markdown/MarkdownContent.js.map +2 -2
- package/dist/portal/PortalShell.js +41 -11
- package/dist/portal/PortalShell.js.map +2 -2
- package/dist/portal/hooks/usePortalDashboardWidgets.js +40 -1
- package/dist/portal/hooks/usePortalDashboardWidgets.js.map +2 -2
- package/dist/portal/utils/nav.js +84 -0
- package/dist/portal/utils/nav.js.map +7 -0
- package/package.json +13 -9
- package/src/backend/AppShell.tsx +22 -2
- package/src/backend/DataTable.tsx +28 -5
- package/src/backend/__tests__/AppShell.test.tsx +67 -0
- package/src/backend/__tests__/FormHeader.test.tsx +0 -1
- package/src/backend/detail/CustomDataSection.tsx +1 -10
- package/src/backend/detail/InlineEditors.tsx +3 -15
- package/src/backend/detail/NotesSection.tsx +5 -14
- package/src/backend/icons/lucideRegistry.generated.tsx +93 -3
- package/src/backend/injection/__tests__/resolveInjectedIcon.test.tsx +7 -0
- package/src/backend/markdown/MarkdownContent.tsx +76 -6
- package/src/backend/section-page/types.ts +1 -0
- package/src/portal/PortalShell.tsx +43 -11
- package/src/portal/hooks/__tests__/usePortalDashboardWidgets.test.tsx +117 -0
- package/src/portal/hooks/usePortalDashboardWidgets.ts +55 -1
- package/src/portal/utils/__tests__/nav.test.ts +199 -0
- package/src/portal/utils/nav.ts +150 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
import { hasAllFeatures } from '@open-mercato/shared/security/features'
|
|
3
|
+
|
|
4
|
+
export type PortalNavGroupId = 'main' | 'account'
|
|
5
|
+
|
|
6
|
+
export type PortalNavItem = {
|
|
7
|
+
id: string
|
|
8
|
+
label: string
|
|
9
|
+
labelKey?: string
|
|
10
|
+
href: string
|
|
11
|
+
icon?: string
|
|
12
|
+
order: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PortalNavGroup = {
|
|
16
|
+
id: PortalNavGroupId
|
|
17
|
+
items: PortalNavItem[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type BuildPortalNavOptions = {
|
|
21
|
+
/** Route manifest to inspect (typically `getFrontendRouteManifests()`). */
|
|
22
|
+
routes: readonly FrontendRouteManifestEntry[]
|
|
23
|
+
/** Current customer org slug — substituted into `[orgSlug]` patterns. */
|
|
24
|
+
orgSlug: string
|
|
25
|
+
/** Feature strings granted to the current customer (may include wildcards). */
|
|
26
|
+
grantedFeatures: readonly string[]
|
|
27
|
+
/** If true, bypass feature checks (portal admin). Defaults to false. */
|
|
28
|
+
isPortalAdmin?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPortalPattern(pattern: string | undefined): pattern is string {
|
|
32
|
+
if (!pattern) return false
|
|
33
|
+
return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasNoUnresolvedParams(href: string): boolean {
|
|
37
|
+
return !href.includes('[')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveHref(pattern: string, orgSlug: string): string {
|
|
41
|
+
return pattern.replace('[orgSlug]', orgSlug)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickGroup(group: unknown): PortalNavGroupId {
|
|
45
|
+
if (group === 'main' || group === 'account') return group
|
|
46
|
+
return 'main'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the portal sidebar from the frontend route manifest.
|
|
51
|
+
*
|
|
52
|
+
* Mirrors `buildAdminNav()` for the portal surface: selects routes under
|
|
53
|
+
* `/[orgSlug]/portal/*` that declare a `nav` block, applies
|
|
54
|
+
* `requireCustomerFeatures` against the caller's grants (wildcards honored),
|
|
55
|
+
* and returns ordered sidebar groups.
|
|
56
|
+
*
|
|
57
|
+
* Absence of `nav` on a metadata file means the page is routable but not
|
|
58
|
+
* auto-listed — useful for detail/create pages.
|
|
59
|
+
*/
|
|
60
|
+
export function buildPortalNav({
|
|
61
|
+
routes,
|
|
62
|
+
orgSlug,
|
|
63
|
+
grantedFeatures,
|
|
64
|
+
isPortalAdmin = false,
|
|
65
|
+
}: BuildPortalNavOptions): PortalNavGroup[] {
|
|
66
|
+
const mainItems: PortalNavItem[] = []
|
|
67
|
+
const accountItems: PortalNavItem[] = []
|
|
68
|
+
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
const pattern = route.pattern ?? route.path
|
|
71
|
+
if (!isPortalPattern(pattern)) continue
|
|
72
|
+
if (route.navHidden) continue
|
|
73
|
+
const nav = route.nav
|
|
74
|
+
if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue
|
|
75
|
+
|
|
76
|
+
const requireFeatures = route.requireCustomerFeatures ?? []
|
|
77
|
+
if (!isPortalAdmin && requireFeatures.length) {
|
|
78
|
+
if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const href = resolveHref(pattern as string, orgSlug)
|
|
82
|
+
if (!hasNoUnresolvedParams(href)) continue
|
|
83
|
+
|
|
84
|
+
const group = pickGroup(nav.group)
|
|
85
|
+
const item: PortalNavItem = {
|
|
86
|
+
id: `portal-nav:${pattern}`,
|
|
87
|
+
label: nav.label,
|
|
88
|
+
labelKey: nav.labelKey,
|
|
89
|
+
href,
|
|
90
|
+
icon: nav.icon,
|
|
91
|
+
order: typeof nav.order === 'number' ? nav.order : 100,
|
|
92
|
+
}
|
|
93
|
+
if (group === 'account') accountItems.push(item)
|
|
94
|
+
else mainItems.push(item)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sortItems = (items: PortalNavItem[]) =>
|
|
98
|
+
items.sort((a, b) => {
|
|
99
|
+
if (a.order !== b.order) return a.order - b.order
|
|
100
|
+
return a.label.localeCompare(b.label)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
sortItems(mainItems)
|
|
104
|
+
sortItems(accountItems)
|
|
105
|
+
|
|
106
|
+
const groups: PortalNavGroup[] = []
|
|
107
|
+
if (mainItems.length) groups.push({ id: 'main', items: mainItems })
|
|
108
|
+
if (accountItems.length) groups.push({ id: 'account', items: accountItems })
|
|
109
|
+
return groups
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Merge sidebar groups from the portal nav endpoint with items contributed via
|
|
114
|
+
* `usePortalInjectedMenuItems`. Auto-discovered entries take precedence —
|
|
115
|
+
* injected items with matching `id` or `href` are dropped as duplicates.
|
|
116
|
+
*/
|
|
117
|
+
export function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(
|
|
118
|
+
discovered: readonly PortalNavGroup[],
|
|
119
|
+
injected: {
|
|
120
|
+
main: readonly TInjected[]
|
|
121
|
+
account: readonly TInjected[]
|
|
122
|
+
},
|
|
123
|
+
): {
|
|
124
|
+
main: Array<PortalNavItem | TInjected>
|
|
125
|
+
account: Array<PortalNavItem | TInjected>
|
|
126
|
+
} {
|
|
127
|
+
const mergeGroup = <T extends PortalNavItem | TInjected>(
|
|
128
|
+
base: readonly PortalNavItem[],
|
|
129
|
+
extra: readonly TInjected[],
|
|
130
|
+
): Array<PortalNavItem | TInjected> => {
|
|
131
|
+
const knownIds = new Set(base.map((item) => item.id))
|
|
132
|
+
const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))
|
|
133
|
+
const merged: Array<PortalNavItem | TInjected> = [...base]
|
|
134
|
+
for (const item of extra) {
|
|
135
|
+
if (knownIds.has(item.id)) continue
|
|
136
|
+
if (item.href && knownHrefs.has(item.href)) continue
|
|
137
|
+
merged.push(item)
|
|
138
|
+
knownIds.add(item.id)
|
|
139
|
+
if (item.href) knownHrefs.add(item.href)
|
|
140
|
+
}
|
|
141
|
+
return merged
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []
|
|
145
|
+
const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []
|
|
146
|
+
return {
|
|
147
|
+
main: mergeGroup(mainBase, injected.main),
|
|
148
|
+
account: mergeGroup(accountBase, injected.account),
|
|
149
|
+
}
|
|
150
|
+
}
|