@elevasis/core 0.35.1 → 0.37.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/dist/auth/index.d.ts +124 -2
- package/dist/auth/index.js +14 -1
- package/dist/index.d.ts +334 -3
- package/dist/index.js +21 -5
- package/dist/knowledge/index.d.ts +81 -0
- package/dist/organization-model/index.d.ts +334 -3
- package/dist/organization-model/index.js +21 -5
- package/dist/test-utils/index.d.ts +97 -0
- package/dist/test-utils/index.js +19 -4
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +67 -0
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +24 -9
- package/src/auth/multi-tenancy/invitations/index.ts +11 -9
- package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +39 -3
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +36 -8
- package/src/auth/multi-tenancy/organizations/index.ts +13 -11
- package/src/business/acquisition/build-templates.test.ts +34 -0
- package/src/business/acquisition/build-templates.ts +8 -1
- package/src/organization-model/__tests__/domains/navigation-topbar.test.ts +282 -0
- package/src/organization-model/__tests__/migration-helpers.test.ts +11 -11
- package/src/organization-model/defaults.ts +2 -1
- package/src/organization-model/domains/navigation.ts +176 -139
- package/src/organization-model/icons.ts +1 -0
- package/src/organization-model/migration-helpers.ts +8 -1
- package/src/organization-model/published.ts +6 -6
- package/src/organization-model/types.ts +5 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2054
- package/src/reference/_generated/contracts.md +67 -0
- package/src/scaffold-registry/index.ts +4 -4
- package/src/supabase/database.types.ts +15 -0
- package/src/test-utils/rls/RLSTestContext.ts +3 -3
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
OrganizationModelNavigationSchema,
|
|
4
|
+
TopbarActionNodeSchema,
|
|
5
|
+
TopbarSectionSchema
|
|
6
|
+
} from '../../domains/navigation'
|
|
7
|
+
import { OrganizationModelSchema } from '../../schema'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function makeMinimalSystems() {
|
|
14
|
+
return {
|
|
15
|
+
dashboard: {
|
|
16
|
+
id: 'dashboard',
|
|
17
|
+
order: 10,
|
|
18
|
+
label: 'Dashboard',
|
|
19
|
+
enabled: true,
|
|
20
|
+
lifecycle: 'active' as const,
|
|
21
|
+
path: '/'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeMinimalModel(overrides: Record<string, unknown> = {}) {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
branding: { organizationName: 'Test', productName: 'Test OS', shortName: 'Test' },
|
|
30
|
+
systems: makeMinimalSystems(),
|
|
31
|
+
entities: {},
|
|
32
|
+
actions: {},
|
|
33
|
+
...overrides
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// TopbarActionNodeSchema
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe('TopbarActionNodeSchema — leaf node shape', () => {
|
|
42
|
+
it('accepts a minimal node (id + label + enabled default)', () => {
|
|
43
|
+
const result = TopbarActionNodeSchema.safeParse({
|
|
44
|
+
id: 'request',
|
|
45
|
+
label: 'Request a feature or report an issue'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(result.success).toBe(true)
|
|
49
|
+
if (result.success) {
|
|
50
|
+
expect(result.data.enabled).toBe(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('accepts a fully populated node', () => {
|
|
55
|
+
const result = TopbarActionNodeSchema.safeParse({
|
|
56
|
+
id: 'request',
|
|
57
|
+
label: 'Request a feature or report an issue',
|
|
58
|
+
tooltip: 'Request a feature or report an issue',
|
|
59
|
+
icon: 'message-plus',
|
|
60
|
+
order: 10,
|
|
61
|
+
enabled: true,
|
|
62
|
+
devOnly: false,
|
|
63
|
+
requiresAdmin: false,
|
|
64
|
+
targets: { systems: ['monitoring'], actions: [] }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(result.success).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('defaults enabled to true when omitted', () => {
|
|
71
|
+
const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs' })
|
|
72
|
+
|
|
73
|
+
expect(result.enabled).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('accepts enabled: false to mark an item as hidden', () => {
|
|
77
|
+
const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs', enabled: false })
|
|
78
|
+
|
|
79
|
+
expect(result.enabled).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts optional devOnly flag', () => {
|
|
83
|
+
const result = TopbarActionNodeSchema.parse({ id: 'devtools', label: 'Dev Tools', devOnly: true })
|
|
84
|
+
|
|
85
|
+
expect(result.devOnly).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('accepts optional requiresAdmin flag', () => {
|
|
89
|
+
const result = TopbarActionNodeSchema.parse({ id: 'admin-panel', label: 'Admin Panel', requiresAdmin: true })
|
|
90
|
+
|
|
91
|
+
expect(result.requiresAdmin).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('rejects a node missing id', () => {
|
|
95
|
+
const result = TopbarActionNodeSchema.safeParse({ label: 'No Id' })
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('rejects a node missing label', () => {
|
|
101
|
+
const result = TopbarActionNodeSchema.safeParse({ id: 'no-label' })
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does NOT have a path field (leaf, not a route)', () => {
|
|
107
|
+
const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
|
|
108
|
+
|
|
109
|
+
expect('path' in result).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('does NOT have a surfaceType field (closed enum untouched)', () => {
|
|
113
|
+
const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
|
|
114
|
+
|
|
115
|
+
expect('surfaceType' in result).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// TopbarSectionSchema
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
describe('TopbarSectionSchema — flat map of topbar action nodes', () => {
|
|
124
|
+
it('defaults to an empty record', () => {
|
|
125
|
+
const result = TopbarSectionSchema.parse(undefined)
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('accepts a populated map', () => {
|
|
131
|
+
const result = TopbarSectionSchema.safeParse({
|
|
132
|
+
request: { id: 'request', label: 'Request' },
|
|
133
|
+
docs: { id: 'docs', label: 'Docs', icon: 'knowledge' }
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(result.success).toBe(true)
|
|
137
|
+
if (result.success) {
|
|
138
|
+
expect(Object.keys(result.data)).toEqual(['request', 'docs'])
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// OrganizationModelNavigationSchema — topbar field added
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
describe('OrganizationModelNavigationSchema — topbar region', () => {
|
|
148
|
+
it('defaults topbar to an empty record', () => {
|
|
149
|
+
const result = OrganizationModelNavigationSchema.parse({})
|
|
150
|
+
|
|
151
|
+
expect(result.topbar).toEqual({})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('retains existing sidebar defaults (regression: append only, sidebar untouched)', () => {
|
|
155
|
+
const result = OrganizationModelNavigationSchema.parse({})
|
|
156
|
+
|
|
157
|
+
expect(result.sidebar.primary).toEqual({})
|
|
158
|
+
expect(result.sidebar.bottom).toEqual({})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('accepts topbar nodes alongside sidebar', () => {
|
|
162
|
+
const result = OrganizationModelNavigationSchema.safeParse({
|
|
163
|
+
sidebar: { primary: {}, bottom: {} },
|
|
164
|
+
topbar: {
|
|
165
|
+
request: {
|
|
166
|
+
id: 'request',
|
|
167
|
+
label: 'Request a feature or report an issue',
|
|
168
|
+
tooltip: 'Request a feature or report an issue',
|
|
169
|
+
icon: 'message-plus',
|
|
170
|
+
order: 10,
|
|
171
|
+
enabled: true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(true)
|
|
177
|
+
if (result.success) {
|
|
178
|
+
expect(result.data.topbar['request']?.id).toBe('request')
|
|
179
|
+
expect(result.data.topbar['request']?.icon).toBe('message-plus')
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// OrganizationModelSchema — navigation.topbar propagates through full schema
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('OrganizationModelSchema — navigation.topbar contract', () => {
|
|
189
|
+
it('accepts a model with topbar declared', () => {
|
|
190
|
+
const result = OrganizationModelSchema.safeParse(
|
|
191
|
+
makeMinimalModel({
|
|
192
|
+
navigation: {
|
|
193
|
+
topbar: {
|
|
194
|
+
request: {
|
|
195
|
+
id: 'request',
|
|
196
|
+
label: 'Request a feature or report an issue',
|
|
197
|
+
tooltip: 'Request a feature or report an issue',
|
|
198
|
+
icon: 'message-plus',
|
|
199
|
+
order: 10,
|
|
200
|
+
enabled: true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(result.success).toBe(true)
|
|
208
|
+
if (result.success) {
|
|
209
|
+
expect(result.data.navigation.topbar['request']?.id).toBe('request')
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('defaults navigation.topbar to empty when navigation is omitted', () => {
|
|
214
|
+
const result = OrganizationModelSchema.safeParse(makeMinimalModel())
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(true)
|
|
217
|
+
if (result.success) {
|
|
218
|
+
expect(result.data.navigation.topbar).toEqual({})
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('defaults navigation.topbar to empty when only sidebar is provided', () => {
|
|
223
|
+
const result = OrganizationModelSchema.safeParse(
|
|
224
|
+
makeMinimalModel({
|
|
225
|
+
navigation: {
|
|
226
|
+
sidebar: {
|
|
227
|
+
primary: {
|
|
228
|
+
dashboard: {
|
|
229
|
+
type: 'surface',
|
|
230
|
+
label: 'Dashboard',
|
|
231
|
+
path: '/',
|
|
232
|
+
surfaceType: 'dashboard',
|
|
233
|
+
order: 10
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
bottom: {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true)
|
|
243
|
+
if (result.success) {
|
|
244
|
+
expect(result.data.navigation.topbar).toEqual({})
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('coexists with sidebar — both sections independently populated', () => {
|
|
249
|
+
const result = OrganizationModelSchema.safeParse(
|
|
250
|
+
makeMinimalModel({
|
|
251
|
+
navigation: {
|
|
252
|
+
sidebar: {
|
|
253
|
+
primary: {
|
|
254
|
+
dashboard: {
|
|
255
|
+
type: 'surface',
|
|
256
|
+
label: 'Dashboard',
|
|
257
|
+
path: '/',
|
|
258
|
+
surfaceType: 'dashboard',
|
|
259
|
+
order: 10
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
bottom: {}
|
|
263
|
+
},
|
|
264
|
+
topbar: {
|
|
265
|
+
request: {
|
|
266
|
+
id: 'request',
|
|
267
|
+
label: 'Request',
|
|
268
|
+
order: 10,
|
|
269
|
+
enabled: true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
expect(result.success).toBe(true)
|
|
277
|
+
if (result.success) {
|
|
278
|
+
expect(result.data.navigation.sidebar.primary['dashboard']?.label).toBe('Dashboard')
|
|
279
|
+
expect(result.data.navigation.topbar['request']?.label).toBe('Request')
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -114,17 +114,6 @@ function modelWithBuildTemplate(): OrganizationModel {
|
|
|
114
114
|
kind: 'template-step',
|
|
115
115
|
appliesTo: 'sales.lead-gen:object/list',
|
|
116
116
|
entries: {
|
|
117
|
-
'source-companies': {
|
|
118
|
-
label: 'Source Companies',
|
|
119
|
-
order: 10,
|
|
120
|
-
primaryEntity: 'company',
|
|
121
|
-
outputs: ['company'],
|
|
122
|
-
stageKey: 'populated',
|
|
123
|
-
dependencyMode: 'per-record-eligibility',
|
|
124
|
-
actionKey: 'lead-gen.company.source',
|
|
125
|
-
defaultBatchSize: 100,
|
|
126
|
-
maxBatchSize: 500
|
|
127
|
-
},
|
|
128
117
|
'qualify-companies': {
|
|
129
118
|
label: 'Qualify Companies',
|
|
130
119
|
order: 20,
|
|
@@ -136,6 +125,17 @@ function modelWithBuildTemplate(): OrganizationModel {
|
|
|
136
125
|
actionKey: 'lead-gen.company.qualify',
|
|
137
126
|
defaultBatchSize: 100,
|
|
138
127
|
maxBatchSize: 500
|
|
128
|
+
},
|
|
129
|
+
'source-companies': {
|
|
130
|
+
label: 'Source Companies',
|
|
131
|
+
order: 10,
|
|
132
|
+
primaryEntity: 'company',
|
|
133
|
+
outputs: ['company'],
|
|
134
|
+
stageKey: 'populated',
|
|
135
|
+
dependencyMode: 'per-record-eligibility',
|
|
136
|
+
actionKey: 'lead-gen.company.source',
|
|
137
|
+
defaultBatchSize: 100,
|
|
138
|
+
maxBatchSize: 500
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
@@ -1,139 +1,176 @@
|
|
|
1
|
-
import { z, type ZodType } from 'zod'
|
|
2
|
-
import { DescriptionSchema, IconNameSchema, LabelSchema, ModelIdSchema, PathSchema } from './shared'
|
|
3
|
-
|
|
4
|
-
export const SurfaceTypeSchema = z
|
|
5
|
-
.enum(['page', 'dashboard', 'graph', 'detail', 'list', 'settings'])
|
|
6
|
-
.meta({ label: 'Surface type', color: 'blue' })
|
|
7
|
-
|
|
8
|
-
export const SurfaceDefinitionSchema = z.object({
|
|
9
|
-
id: ModelIdSchema,
|
|
10
|
-
label: LabelSchema,
|
|
11
|
-
path: PathSchema,
|
|
12
|
-
surfaceType: SurfaceTypeSchema,
|
|
13
|
-
description: DescriptionSchema.optional(),
|
|
14
|
-
enabled: z.boolean().default(true),
|
|
15
|
-
devOnly: z.boolean().optional(),
|
|
16
|
-
icon: IconNameSchema.optional(),
|
|
17
|
-
systemIds: z.array(ModelIdSchema.meta({ ref: 'system' })).default([]),
|
|
18
|
-
entityIds: z.array(ModelIdSchema.meta({ ref: 'entity' })).default([]),
|
|
19
|
-
resourceIds: z.array(ModelIdSchema.meta({ ref: 'resource' })).default([]),
|
|
20
|
-
actionIds: z.array(ModelIdSchema.meta({ ref: 'action' })).default([]),
|
|
21
|
-
parentId: ModelIdSchema.meta({ ref: 'surface' }).optional()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
export const SidebarSurfaceTargetsSchema = z
|
|
25
|
-
.object({
|
|
26
|
-
systems: z
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
export
|
|
130
|
-
|
|
131
|
-
export const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1
|
+
import { z, type ZodType } from 'zod'
|
|
2
|
+
import { DescriptionSchema, IconNameSchema, LabelSchema, ModelIdSchema, PathSchema } from './shared'
|
|
3
|
+
|
|
4
|
+
export const SurfaceTypeSchema = z
|
|
5
|
+
.enum(['page', 'dashboard', 'graph', 'detail', 'list', 'settings'])
|
|
6
|
+
.meta({ label: 'Surface type', color: 'blue' })
|
|
7
|
+
|
|
8
|
+
export const SurfaceDefinitionSchema = z.object({
|
|
9
|
+
id: ModelIdSchema,
|
|
10
|
+
label: LabelSchema,
|
|
11
|
+
path: PathSchema,
|
|
12
|
+
surfaceType: SurfaceTypeSchema,
|
|
13
|
+
description: DescriptionSchema.optional(),
|
|
14
|
+
enabled: z.boolean().default(true),
|
|
15
|
+
devOnly: z.boolean().optional(),
|
|
16
|
+
icon: IconNameSchema.optional(),
|
|
17
|
+
systemIds: z.array(ModelIdSchema.meta({ ref: 'system' })).default([]),
|
|
18
|
+
entityIds: z.array(ModelIdSchema.meta({ ref: 'entity' })).default([]),
|
|
19
|
+
resourceIds: z.array(ModelIdSchema.meta({ ref: 'resource' })).default([]),
|
|
20
|
+
actionIds: z.array(ModelIdSchema.meta({ ref: 'action' })).default([]),
|
|
21
|
+
parentId: ModelIdSchema.meta({ ref: 'surface' }).optional()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const SidebarSurfaceTargetsSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
systems: z
|
|
27
|
+
.array(ModelIdSchema.meta({ ref: 'system' }))
|
|
28
|
+
.default([])
|
|
29
|
+
.optional(),
|
|
30
|
+
entities: z
|
|
31
|
+
.array(ModelIdSchema.meta({ ref: 'entity' }))
|
|
32
|
+
.default([])
|
|
33
|
+
.optional(),
|
|
34
|
+
resources: z
|
|
35
|
+
.array(ModelIdSchema.meta({ ref: 'resource' }))
|
|
36
|
+
.default([])
|
|
37
|
+
.optional(),
|
|
38
|
+
actions: z
|
|
39
|
+
.array(ModelIdSchema.meta({ ref: 'action' }))
|
|
40
|
+
.default([])
|
|
41
|
+
.optional()
|
|
42
|
+
})
|
|
43
|
+
.default({})
|
|
44
|
+
|
|
45
|
+
export interface SidebarSurfaceNode {
|
|
46
|
+
type: 'surface'
|
|
47
|
+
label: string
|
|
48
|
+
path: string
|
|
49
|
+
surfaceType: z.infer<typeof SurfaceTypeSchema>
|
|
50
|
+
description?: string
|
|
51
|
+
icon?: string
|
|
52
|
+
order?: number
|
|
53
|
+
targets?: {
|
|
54
|
+
systems?: string[]
|
|
55
|
+
entities?: string[]
|
|
56
|
+
resources?: string[]
|
|
57
|
+
actions?: string[]
|
|
58
|
+
}
|
|
59
|
+
devOnly?: boolean
|
|
60
|
+
requiresAdmin?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SidebarGroupNode {
|
|
64
|
+
type: 'group'
|
|
65
|
+
label: string
|
|
66
|
+
description?: string
|
|
67
|
+
icon?: string
|
|
68
|
+
order?: number
|
|
69
|
+
children: Record<string, SidebarNode>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type SidebarNode = SidebarSurfaceNode | SidebarGroupNode
|
|
73
|
+
|
|
74
|
+
export const SidebarNodeSchema: ZodType<SidebarNode> = z.lazy(() =>
|
|
75
|
+
z.discriminatedUnion('type', [
|
|
76
|
+
z.object({
|
|
77
|
+
type: z.literal('group'),
|
|
78
|
+
label: LabelSchema,
|
|
79
|
+
description: DescriptionSchema.optional(),
|
|
80
|
+
icon: IconNameSchema.optional(),
|
|
81
|
+
order: z.number().int().optional(),
|
|
82
|
+
children: z.record(z.string(), SidebarNodeSchema).default({})
|
|
83
|
+
}),
|
|
84
|
+
z.object({
|
|
85
|
+
type: z.literal('surface'),
|
|
86
|
+
label: LabelSchema,
|
|
87
|
+
path: PathSchema,
|
|
88
|
+
surfaceType: SurfaceTypeSchema,
|
|
89
|
+
description: DescriptionSchema.optional(),
|
|
90
|
+
icon: IconNameSchema.optional(),
|
|
91
|
+
order: z.number().int().optional(),
|
|
92
|
+
targets: SidebarSurfaceTargetsSchema.optional(),
|
|
93
|
+
devOnly: z.boolean().optional(),
|
|
94
|
+
requiresAdmin: z.boolean().optional()
|
|
95
|
+
})
|
|
96
|
+
])
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
export const SidebarSectionSchema = z.record(z.string(), SidebarNodeSchema).default({})
|
|
100
|
+
|
|
101
|
+
export const SidebarNavigationSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
primary: SidebarSectionSchema,
|
|
104
|
+
bottom: SidebarSectionSchema
|
|
105
|
+
})
|
|
106
|
+
.default({ primary: {}, bottom: {} })
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A leaf node in `navigation.topbar`. Has no `path`, `surfaceType`, or nesting —
|
|
110
|
+
* topbar actions trigger behavior (open a modal, open docs, etc.) rather than navigate to a route.
|
|
111
|
+
* Behavior is bound by a UI registry (`TopbarActionModule`) that matches on `id`.
|
|
112
|
+
*/
|
|
113
|
+
export const TopbarActionNodeSchema = z.object({
|
|
114
|
+
id: ModelIdSchema,
|
|
115
|
+
label: LabelSchema,
|
|
116
|
+
tooltip: DescriptionSchema.optional(),
|
|
117
|
+
icon: IconNameSchema.optional(),
|
|
118
|
+
order: z.number().int().optional(),
|
|
119
|
+
enabled: z.boolean().default(true),
|
|
120
|
+
devOnly: z.boolean().optional(),
|
|
121
|
+
requiresAdmin: z.boolean().optional(),
|
|
122
|
+
targets: SidebarSurfaceTargetsSchema.optional()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A flat map of topbar action nodes, keyed by action id.
|
|
127
|
+
* Defaults to an empty record (no topbar actions declared).
|
|
128
|
+
*/
|
|
129
|
+
export const TopbarSectionSchema = z.record(z.string(), TopbarActionNodeSchema).default({})
|
|
130
|
+
|
|
131
|
+
export const OrganizationModelNavigationSchema = z
|
|
132
|
+
.object({
|
|
133
|
+
sidebar: SidebarNavigationSchema,
|
|
134
|
+
topbar: TopbarSectionSchema
|
|
135
|
+
})
|
|
136
|
+
.default({ sidebar: { primary: {}, bottom: {} }, topbar: {} })
|
|
137
|
+
|
|
138
|
+
export const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: z.infer<typeof OrganizationModelNavigationSchema> = {
|
|
139
|
+
sidebar: {
|
|
140
|
+
primary: {},
|
|
141
|
+
bottom: {}
|
|
142
|
+
},
|
|
143
|
+
topbar: {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getSortedSidebarEntries<TNode extends SidebarNode>(
|
|
147
|
+
nodes: Record<string, TNode>
|
|
148
|
+
): Array<[string, TNode]> {
|
|
149
|
+
return Object.entries(nodes).sort(([leftId, left], [rightId, right]) => {
|
|
150
|
+
const orderDelta = (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER)
|
|
151
|
+
return orderDelta === 0 ? leftId.localeCompare(rightId) : orderDelta
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Core placement values: 'primary' | 'secondary' | 'bottom'.
|
|
157
|
+
*
|
|
158
|
+
* `placement` is intentionally open (`z.string()`) so that per-project adapters can
|
|
159
|
+
* introduce additional placement IDs (e.g. 'pinned', 'spotlight', 'contextual') without
|
|
160
|
+
* forking core. Extension pattern: add the new string literal to the project's
|
|
161
|
+
* `foundations/config/organization-model.ts` adapter — no changes to this file required.
|
|
162
|
+
*
|
|
163
|
+
* `surfaceType` and `resourceType` remain closed enums (UI/runtime invariants).
|
|
164
|
+
*/
|
|
165
|
+
export const CORE_PLACEMENT_VALUES = ['primary', 'secondary', 'bottom'] as const
|
|
166
|
+
export type CorePlacement = (typeof CORE_PLACEMENT_VALUES)[number]
|
|
167
|
+
|
|
168
|
+
export const NavigationGroupSchema = z.object({
|
|
169
|
+
id: ModelIdSchema,
|
|
170
|
+
label: LabelSchema,
|
|
171
|
+
placement: z.string().trim().min(1).max(50),
|
|
172
|
+
surfaceIds: z.array(ModelIdSchema.meta({ ref: 'surface' })).default([])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// SurfaceDefinitionSchema and NavigationGroupSchema remain for derived DTOs and
|
|
176
|
+
// transitional call sites. They are not authored top-level OrganizationModel fields.
|
|
@@ -124,7 +124,14 @@ export function getAllBuildTemplates(model: OrganizationModel): BuildTemplate[]
|
|
|
124
124
|
entriesOf(catalog).map(([templateId, templateEntry]) => {
|
|
125
125
|
const stepCatalogId = stringValue(templateEntry.stepCatalog) as OntologyId | undefined
|
|
126
126
|
const stepCatalog = stepCatalogId !== undefined ? stepCatalogs.get(stepCatalogId) : undefined
|
|
127
|
-
const steps =
|
|
127
|
+
const steps =
|
|
128
|
+
stepCatalog === undefined
|
|
129
|
+
? []
|
|
130
|
+
: entriesOf(stepCatalog).sort(
|
|
131
|
+
([leftId, left], [rightId, right]) =>
|
|
132
|
+
numberValue(left.order, Number.MAX_SAFE_INTEGER) -
|
|
133
|
+
numberValue(right.order, Number.MAX_SAFE_INTEGER) || leftId.localeCompare(rightId)
|
|
134
|
+
)
|
|
128
135
|
|
|
129
136
|
return {
|
|
130
137
|
order: numberValue(templateEntry.order, Number.MAX_SAFE_INTEGER),
|