@ossy/app 1.40.2 → 3.0.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/README.md +49 -12
- package/cli/build.task.js +49 -10
- package/cli/get-platform-files.task.js +21 -15
- package/cli/manifest-plugin.js +340 -48
- package/package.json +25 -13
- package/runtime/merge-shell-slots.js +148 -0
- package/runtime/page-runtime.js +36 -18
- package/runtime/resolve-app-slots.js +126 -0
- package/src/en.translations.json +8 -0
- package/src/manifest/action-id-to-tool-name.js +29 -0
- package/src/manifest/build-action-input-schema.js +220 -0
- package/src/manifest/build-actions-schema.js +36 -0
- package/src/manifest/build-capabilities.js +190 -0
- package/src/manifest/build-manifest-summary.js +11 -5
- package/src/manifest/extract-task-catalog.js +1 -0
- package/src/manifest/resolve-page-layout.js +51 -0
- package/src/manifest/serialize-package-definition.js +2 -3
- package/src/manifest/template-to-json-schema.js +103 -0
- package/src/shell/App.jsx +18 -9
- package/src/shell/AppSettings.jsx +17 -0
- package/src/shell/DevPagesPanel.jsx +17 -13
- package/src/shell/HeaderAuthActions.jsx +49 -0
- package/src/shell/LanguageList.jsx +83 -0
- package/src/shell/LanguageSelect.jsx +73 -0
- package/src/shell/ThemeEditor.jsx +19 -34
- package/src/shell/ThemeSelect.jsx +101 -0
- package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
- package/src/shell/buildSidebarNav.js +113 -0
- package/src/shell/index.js +11 -0
- package/src/shell/languageCode.js +31 -0
- package/src/shell/patchUserAppSettings.js +10 -0
- package/src/shell/resolveEndpoints.js +43 -0
- package/src/shell/resolveWorkspaceServices.js +20 -0
- package/src/shell/themeEditorStyles.js +49 -0
- package/src/shell/useCompactShellLayout.js +24 -0
- package/src/shell/useShellWorkspace.js +40 -0
- package/src/shell-registry/blank.layout.jsx +8 -0
- package/src/shell-registry/default.layout.jsx +101 -0
- package/src/shell-registry/footer-default.component.jsx +39 -0
- package/src/shell-registry/head-default.component.jsx +17 -0
- package/src/shell-registry/header-default.component.jsx +50 -0
- package/src/shell-registry/logo-logomark.component.jsx +8 -0
- package/src/shell-registry/logo-logotype.component.jsx +15 -0
- package/src/shell-registry/minimal.layout.jsx +58 -0
- package/src/shell-registry/sidebar-default.component.jsx +281 -0
- package/src/sv.translations.json +8 -0
- package/runtime/resolve-shell-slots.js +0 -112
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { actionIdToToolName, humanizeActionId } from './action-id-to-tool-name.js'
|
|
2
|
+
import { buildActionInputSchema } from './build-action-input-schema.js'
|
|
3
|
+
import { taskIdFromActionId } from '@ossy/schema'
|
|
4
|
+
|
|
5
|
+
const MCP_ACCESS = new Set(['workspace', 'public'])
|
|
6
|
+
|
|
7
|
+
/** MCP resource URI for read-only task topology (tasks + graph edges). */
|
|
8
|
+
export const TASK_TOPOLOGY_RESOURCE_URI = 'ossy://capabilities/task-topology'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} task
|
|
12
|
+
*/
|
|
13
|
+
export function projectTaskForCapabilities (task) {
|
|
14
|
+
return {
|
|
15
|
+
id: task.id,
|
|
16
|
+
package: task.package ?? null,
|
|
17
|
+
moduleId: task.moduleId ?? null,
|
|
18
|
+
triggers: task.triggers ?? [],
|
|
19
|
+
schedule: task.schedule ?? null,
|
|
20
|
+
inputSchemaIds: task.inputSchemaIds ?? [],
|
|
21
|
+
actionIds: task.actionIds ?? [],
|
|
22
|
+
primaryActionId: task.primaryActionId ?? null,
|
|
23
|
+
outputs: task.outputs ?? [],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Structured trigger → task edges for agent planning (ADR 0011).
|
|
29
|
+
*
|
|
30
|
+
* @param {object[]} taskCatalog
|
|
31
|
+
*/
|
|
32
|
+
export function buildCapabilitiesGraph (taskCatalog = []) {
|
|
33
|
+
/** @type {Array<{ kind: string, from: object, to: { taskId: string }, label?: string }>} */
|
|
34
|
+
const edges = []
|
|
35
|
+
|
|
36
|
+
for (const task of taskCatalog) {
|
|
37
|
+
for (const trigger of task.triggers ?? []) {
|
|
38
|
+
if (trigger.kind === 'on_action' && trigger.action) {
|
|
39
|
+
edges.push({
|
|
40
|
+
kind: 'on_action',
|
|
41
|
+
from: { kind: 'action', actionId: trigger.action },
|
|
42
|
+
to: { taskId: task.id },
|
|
43
|
+
label: trigger.action,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
if (trigger.kind === 'on_event' && trigger.type) {
|
|
47
|
+
edges.push({
|
|
48
|
+
kind: 'on_event',
|
|
49
|
+
from: {
|
|
50
|
+
kind: 'trigger',
|
|
51
|
+
type: trigger.type,
|
|
52
|
+
...(trigger.event ? { event: trigger.event } : {}),
|
|
53
|
+
},
|
|
54
|
+
to: { taskId: task.id },
|
|
55
|
+
label: [trigger.type, trigger.event].filter(Boolean).join(' · '),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
if (trigger.kind === 'on_schedule' && trigger.schedule) {
|
|
59
|
+
edges.push({
|
|
60
|
+
kind: 'on_schedule',
|
|
61
|
+
from: { kind: 'schedule', schedule: trigger.schedule },
|
|
62
|
+
to: { taskId: task.id },
|
|
63
|
+
label: trigger.schedule,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (task.primaryActionId) {
|
|
68
|
+
edges.push({
|
|
69
|
+
kind: 'primary',
|
|
70
|
+
from: { kind: 'action', actionId: task.primaryActionId },
|
|
71
|
+
to: { taskId: task.id },
|
|
72
|
+
label: 'invoke',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { edges }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build agent capabilities (MCP tools + resource template catalog) from manifest data.
|
|
82
|
+
*
|
|
83
|
+
* @param {{
|
|
84
|
+
* actions?: Array<{ id: string, access?: string, package?: string }>,
|
|
85
|
+
* tasks?: Array<{ id: string }>,
|
|
86
|
+
* schemas?: Array<object>,
|
|
87
|
+
* taskCatalog?: object[],
|
|
88
|
+
* taskGraphEdges?: object[],
|
|
89
|
+
* }} manifest
|
|
90
|
+
* @param {{ generatedAt?: string }} [options]
|
|
91
|
+
*/
|
|
92
|
+
export function buildCapabilities (manifest, options = {}) {
|
|
93
|
+
const actions = manifest.actions || []
|
|
94
|
+
const tasks = manifest.tasks || []
|
|
95
|
+
const schemas = manifest.schemas || []
|
|
96
|
+
const taskCatalog = manifest.taskCatalog || []
|
|
97
|
+
const taskIds = new Set(tasks.map(t => t.id))
|
|
98
|
+
|
|
99
|
+
const tools = actions
|
|
100
|
+
.filter(action => {
|
|
101
|
+
try {
|
|
102
|
+
return taskIds.has(taskIdFromActionId(action.id))
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.filter(action => MCP_ACCESS.has(action.access || 'authenticated'))
|
|
108
|
+
.map(action => ({
|
|
109
|
+
actionId: action.id,
|
|
110
|
+
name: actionIdToToolName(action.id),
|
|
111
|
+
title: humanizeActionId(action.id),
|
|
112
|
+
description: `Invoke ${action.id}`,
|
|
113
|
+
access: action.access || 'authenticated',
|
|
114
|
+
package: action.package,
|
|
115
|
+
channels: ['sdk', 'api', 'mcp'],
|
|
116
|
+
inputSchema: buildActionInputSchema(action.id, { schemas }),
|
|
117
|
+
}))
|
|
118
|
+
.sort((a, b) => a.actionId.localeCompare(b.actionId))
|
|
119
|
+
|
|
120
|
+
const projectedTasks = taskCatalog.map(projectTaskForCapabilities)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
version: 2,
|
|
124
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
125
|
+
tools,
|
|
126
|
+
schemas: schemas.map(t => ({
|
|
127
|
+
id: t.id,
|
|
128
|
+
name: t.name,
|
|
129
|
+
package: t.package,
|
|
130
|
+
fields: t.fields,
|
|
131
|
+
})),
|
|
132
|
+
tasks: projectedTasks,
|
|
133
|
+
graph: buildCapabilitiesGraph(taskCatalog),
|
|
134
|
+
/** Legacy string-edge projection kept for UI parity — prefer `graph.edges`. */
|
|
135
|
+
taskGraphEdges: manifest.taskGraphEdges ?? [],
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Payload exposed via MCP resource {@link TASK_TOPOLOGY_RESOURCE_URI}.
|
|
141
|
+
*
|
|
142
|
+
* @param {ReturnType<typeof buildCapabilities>} capabilities
|
|
143
|
+
*/
|
|
144
|
+
export function capabilitiesTaskTopologyResource (capabilities) {
|
|
145
|
+
return {
|
|
146
|
+
version: capabilities.version,
|
|
147
|
+
generatedAt: capabilities.generatedAt,
|
|
148
|
+
tasks: capabilities.tasks ?? [],
|
|
149
|
+
graph: capabilities.graph ?? { edges: [] },
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Legacy actions.schema.json shape for package detail / docs.
|
|
155
|
+
*
|
|
156
|
+
* @param {ReturnType<typeof buildCapabilities>} capabilities
|
|
157
|
+
* @param {Array<{ id: string, access?: string, package?: string }>} [allActions]
|
|
158
|
+
*/
|
|
159
|
+
export function capabilitiesToActionsSchema (capabilities, allActions) {
|
|
160
|
+
const toolById = Object.fromEntries(capabilities.tools.map(t => [t.actionId, t]))
|
|
161
|
+
const actions = (allActions || capabilities.tools.map(t => ({
|
|
162
|
+
id: t.actionId,
|
|
163
|
+
access: t.access,
|
|
164
|
+
package: t.package,
|
|
165
|
+
})))
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
version: capabilities.version,
|
|
169
|
+
generatedAt: capabilities.generatedAt,
|
|
170
|
+
actions: actions.map(action => {
|
|
171
|
+
const tool = toolById[action.id]
|
|
172
|
+
const inputSchema = tool?.inputSchema
|
|
173
|
+
?? buildActionInputSchema(action.id, {
|
|
174
|
+
schemas: capabilities.schemas,
|
|
175
|
+
})
|
|
176
|
+
const hasKnownEnvelope = inputSchema?.properties && !inputSchema.properties.payload
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: action.id,
|
|
180
|
+
access: action.access ?? 'authenticated',
|
|
181
|
+
package: action.package,
|
|
182
|
+
channels: tool ? tool.channels : ['sdk', 'api'],
|
|
183
|
+
...(tool?.title ? { title: tool.title } : {}),
|
|
184
|
+
...(tool?.description ? { description: tool.description } : {}),
|
|
185
|
+
...(tool?.name ? { mcpTool: tool.name } : {}),
|
|
186
|
+
...(hasKnownEnvelope ? { inputSchema } : {}),
|
|
187
|
+
}
|
|
188
|
+
}).sort((a, b) => a.id.localeCompare(b.id)),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -32,7 +32,7 @@ export function buildManifestSummary (manifest) {
|
|
|
32
32
|
apis: [],
|
|
33
33
|
actions: [],
|
|
34
34
|
components: [],
|
|
35
|
-
|
|
35
|
+
schemas: [],
|
|
36
36
|
tasks: [],
|
|
37
37
|
integrations: [],
|
|
38
38
|
emails: [],
|
|
@@ -69,15 +69,21 @@ export function buildManifestSummary (manifest) {
|
|
|
69
69
|
add(component.package, 'components', { id: component.id })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
for (const template of manifest.
|
|
73
|
-
add(template.package, '
|
|
72
|
+
for (const template of manifest.schemas || []) {
|
|
73
|
+
add(template.package, 'schemas', {
|
|
74
74
|
id: template.id,
|
|
75
75
|
...(template.title ? { title: template.title } : {}),
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
for (const task of manifest.tasks || []) {
|
|
80
|
-
add(task.package, 'tasks', {
|
|
79
|
+
for (const task of manifest.taskCatalog || manifest.tasks || []) {
|
|
80
|
+
add(task.package, 'tasks', {
|
|
81
|
+
id: task.id,
|
|
82
|
+
...(task.moduleId ? { moduleId: task.moduleId } : {}),
|
|
83
|
+
...(task.triggers ? { triggers: task.triggers } : {}),
|
|
84
|
+
...(task.inputSchemaIds ? { inputSchemaIds: task.inputSchemaIds } : {}),
|
|
85
|
+
...(task.primaryActionId ? { primaryActionId: task.primaryActionId } : {}),
|
|
86
|
+
})
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
for (const integration of manifest.integrations || []) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { extractTaskCatalogEntry, buildTaskCatalogEdges } from '@ossy/schema'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mergeShellSlots } from '../../runtime/merge-shell-slots.js'
|
|
2
|
+
|
|
3
|
+
const LAYOUT_ID_PATTERN = /^@([^/]+)\/([^/]+)\/layout\/([^/]+)$/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} layoutId
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function isCanonicalLayoutId (layoutId) {
|
|
10
|
+
return typeof layoutId === 'string' && LAYOUT_ID_PATTERN.test(layoutId.trim())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve layout id for a page at build time (ADR 0012).
|
|
15
|
+
*
|
|
16
|
+
* @param {{
|
|
17
|
+
* pageMetadata?: { layout?: string },
|
|
18
|
+
* appConfig?: { layout?: string },
|
|
19
|
+
* }} options
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePageLayoutId ({
|
|
23
|
+
pageMetadata = {},
|
|
24
|
+
appConfig = {},
|
|
25
|
+
} = {}) {
|
|
26
|
+
const pageLayout = pageMetadata?.layout
|
|
27
|
+
if (typeof pageLayout === 'string' && pageLayout.trim()) return pageLayout.trim()
|
|
28
|
+
|
|
29
|
+
const appLayout = appConfig?.layout
|
|
30
|
+
if (typeof appLayout === 'string' && appLayout.trim()) return appLayout.trim()
|
|
31
|
+
|
|
32
|
+
return '@ossy/app/layout/default'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merge layout, app, and page slot maps for a single page render.
|
|
37
|
+
*
|
|
38
|
+
* @param {{
|
|
39
|
+
* layoutSlots?: Record<string, string>,
|
|
40
|
+
* appSlots?: Record<string, string | null>,
|
|
41
|
+
* pageSlots?: Record<string, string | null>,
|
|
42
|
+
* }} options
|
|
43
|
+
* @returns {Record<string, import('../../runtime/merge-shell-slots.js').ShellSlotSpec>}
|
|
44
|
+
*/
|
|
45
|
+
export function resolvePageShellSlots ({
|
|
46
|
+
layoutSlots,
|
|
47
|
+
appSlots,
|
|
48
|
+
pageSlots,
|
|
49
|
+
} = {}) {
|
|
50
|
+
return mergeShellSlots(layoutSlots, appSlots, pageSlots)
|
|
51
|
+
}
|
|
@@ -21,9 +21,8 @@ export function serializePackageDefinition (definition) {
|
|
|
21
21
|
out.navOrder = definition.navOrder
|
|
22
22
|
}
|
|
23
23
|
if (definition.entitlementRequired === false) out.entitlementRequired = false
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
out.statuses = definition.statuses.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim())
|
|
24
|
+
if (Array.isArray(definition.status) && definition.status.length) {
|
|
25
|
+
out.status = definition.status.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim())
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
return Object.keys(out).length ? out : null
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map resource template field types to JSON Schema (draft-07 subset).
|
|
3
|
+
*
|
|
4
|
+
* @param {{ name: string, type: string, options?: string[] }} field
|
|
5
|
+
* @returns {object}
|
|
6
|
+
*/
|
|
7
|
+
export function fieldToJsonSchema (field) {
|
|
8
|
+
const type = field?.type?.trim?.() || 'text'
|
|
9
|
+
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'number':
|
|
12
|
+
case 'timestamp':
|
|
13
|
+
return { type: 'number', description: `${field.name} (Unix ms for timestamp)` }
|
|
14
|
+
case 'boolean':
|
|
15
|
+
return { type: 'boolean' }
|
|
16
|
+
case 'select':
|
|
17
|
+
return {
|
|
18
|
+
type: 'string',
|
|
19
|
+
...(Array.isArray(field.options) && field.options.length
|
|
20
|
+
? { enum: field.options }
|
|
21
|
+
: {}),
|
|
22
|
+
}
|
|
23
|
+
case 'multiselect':
|
|
24
|
+
return {
|
|
25
|
+
type: 'array',
|
|
26
|
+
items: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
...(Array.isArray(field.options) && field.options.length
|
|
29
|
+
? { enum: field.options }
|
|
30
|
+
: {}),
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
case 'file':
|
|
34
|
+
case 'image':
|
|
35
|
+
return {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: { resourceId: { type: 'string' } },
|
|
38
|
+
required: ['resourceId'],
|
|
39
|
+
description: 'Reference to an uploaded binary resource',
|
|
40
|
+
}
|
|
41
|
+
case 'date-range':
|
|
42
|
+
return {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
start: { type: 'number' },
|
|
46
|
+
end: { type: 'number' },
|
|
47
|
+
},
|
|
48
|
+
required: ['start', 'end'],
|
|
49
|
+
}
|
|
50
|
+
case 'date-ranges':
|
|
51
|
+
return {
|
|
52
|
+
type: 'array',
|
|
53
|
+
items: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
start: { type: 'number' },
|
|
57
|
+
end: { type: 'number' },
|
|
58
|
+
},
|
|
59
|
+
required: ['start', 'end'],
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
case 'email':
|
|
63
|
+
return { type: 'string', format: 'email' }
|
|
64
|
+
case 'textarea':
|
|
65
|
+
case 'richtext':
|
|
66
|
+
case 'text':
|
|
67
|
+
case 'date':
|
|
68
|
+
default:
|
|
69
|
+
return { type: 'string' }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {{ id: string, name?: string, fields?: Array<object> }} template
|
|
75
|
+
* @returns {object}
|
|
76
|
+
*/
|
|
77
|
+
export function templateToJsonSchema (template) {
|
|
78
|
+
const properties = {}
|
|
79
|
+
for (const field of template.fields || []) {
|
|
80
|
+
if (!field?.name) continue
|
|
81
|
+
properties[field.name] = fieldToJsonSchema(field)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
title: template.id,
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {Array<{ id: string, fields?: Array<object> }>} templates
|
|
93
|
+
* @returns {object | undefined}
|
|
94
|
+
*/
|
|
95
|
+
export function templatesContentOneOf (templates) {
|
|
96
|
+
const branches = (templates || [])
|
|
97
|
+
.filter(t => t?.id && Array.isArray(t.fields))
|
|
98
|
+
.map(templateToJsonSchema)
|
|
99
|
+
|
|
100
|
+
if (!branches.length) return undefined
|
|
101
|
+
|
|
102
|
+
return { oneOf: branches }
|
|
103
|
+
}
|
package/src/shell/App.jsx
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { SDK } from '@ossy/sdk'
|
|
3
3
|
import { WorkspaceProvider } from '@ossy/sdk-react'
|
|
4
|
-
import { Theme, ComponentSlotsProvider, LocaleProvider } from '@ossy/design-system'
|
|
4
|
+
import { Theme, ComponentSlotsProvider, LocaleProvider, DEFAULT_FORM_FIELD_SLOTS } from '@ossy/design-system'
|
|
5
|
+
import { DEFAULT_SCHEMA_VIEW_SLOTS } from '@ossy/resources/schemaViewSlots.js'
|
|
6
|
+
import { DEFAULT_SCHEMA_FORM_SLOTS } from '@ossy/resources/schemaFormSlots.js'
|
|
7
|
+
import { SchemasReadBootstrap } from '@ossy/workspaces/SchemasReadBootstrap'
|
|
5
8
|
import { ThemeEditor } from './ThemeEditor.jsx'
|
|
9
|
+
import { WorkspaceAppSettingsSync } from './WorkspaceAppSettingsSync.jsx'
|
|
6
10
|
import { defaultAppSettings } from './AppSettings.jsx'
|
|
7
11
|
import { Router } from '@ossy/router-react'
|
|
8
12
|
import { AppContext } from './AppContext.js'
|
|
9
13
|
|
|
10
|
-
export const App = ({ children, language, messages, fallbackMessages, ..._appSettings }) => {
|
|
14
|
+
export const App = ({ children, language, messages, fallbackMessages, sdk: sdkOverride, ..._appSettings }) => {
|
|
11
15
|
const appSettings = { ...defaultAppSettings(), ..._appSettings }
|
|
12
16
|
|
|
13
|
-
// `components`
|
|
14
|
-
|
|
15
|
-
const { components, ...contextSettings } = appSettings
|
|
17
|
+
// `components` and `sdk` are not JSON-serializable — keep them out of AppContext.
|
|
18
|
+
const { components, sdk: _sdkFromSettings, ...contextSettings } = appSettings
|
|
16
19
|
|
|
17
|
-
const sdk = SDK.of({
|
|
20
|
+
const sdk = sdkOverride ?? SDK.of({
|
|
18
21
|
apiUrl: appSettings.apiUrl,
|
|
19
|
-
workspaceId: appSettings.workspaceId
|
|
22
|
+
workspaceId: appSettings.workspaceId,
|
|
23
|
+
actionRoutes: appSettings.actionRoutes,
|
|
20
24
|
})
|
|
21
25
|
|
|
22
26
|
return (
|
|
@@ -28,9 +32,14 @@ export const App = ({ children, language, messages, fallbackMessages, ..._appSet
|
|
|
28
32
|
supportedLanguages={appSettings.supportedLanguages}
|
|
29
33
|
>
|
|
30
34
|
<AppContext.Provider value={contextSettings}>
|
|
31
|
-
<ComponentSlotsProvider slots={components || {}}>
|
|
35
|
+
<ComponentSlotsProvider slots={{ ...DEFAULT_FORM_FIELD_SLOTS, ...DEFAULT_SCHEMA_VIEW_SLOTS, ...DEFAULT_SCHEMA_FORM_SLOTS, ...(components || {}) }}>
|
|
32
36
|
<Theme theme={appSettings.theme} themes={appSettings.themes}>
|
|
33
|
-
<WorkspaceProvider
|
|
37
|
+
<WorkspaceProvider
|
|
38
|
+
sdk={sdk}
|
|
39
|
+
enablePushInvalidation={appSettings.enablePushInvalidation === true}
|
|
40
|
+
>
|
|
41
|
+
<SchemasReadBootstrap schemas={appSettings.schemas} />
|
|
42
|
+
<WorkspaceAppSettingsSync />
|
|
34
43
|
<Router {...appSettings} pages={appSettings.pages || []}>
|
|
35
44
|
{children}
|
|
36
45
|
{appSettings.devMode && <ThemeEditor />}
|
|
@@ -25,7 +25,24 @@ export function defaultAppSettings() {
|
|
|
25
25
|
faviconHref: undefined,
|
|
26
26
|
/** When true, main app sidebar is collapsed to icons (from server cookie / app-settings). */
|
|
27
27
|
sidebarPrimaryCollapsed: false,
|
|
28
|
+
/** Cached workspace shell snapshot (from user-app-settings cookie; refreshed client-side). */
|
|
29
|
+
workspaceName: undefined,
|
|
30
|
+
workspaceServices: undefined,
|
|
31
|
+
workspaces: undefined,
|
|
28
32
|
/** Grouped manifest entries by npm package — for dev tooling. */
|
|
29
33
|
manifestSummary: undefined,
|
|
34
|
+
/** Merged system + workspace schemas (SSR bootstrap for client validation). */
|
|
35
|
+
schemas: undefined,
|
|
36
|
+
/** Task catalog from `app build` (ADR 0011) — for automation topology UI. */
|
|
37
|
+
taskCatalog: undefined,
|
|
38
|
+
taskGraphEdges: undefined,
|
|
39
|
+
/** Action id → HTTP route map for sdk.invoke transport routing. */
|
|
40
|
+
actionRoutes: undefined,
|
|
41
|
+
/**
|
|
42
|
+
* When true, the app shell opens SSE push invalidation for every page.
|
|
43
|
+
* Default false — mount `PushInvalidationSubscriber` only on pages that
|
|
44
|
+
* need live cross-tab cache updates (see @ossy/sdk-react README).
|
|
45
|
+
*/
|
|
46
|
+
enablePushInvalidation: false,
|
|
30
47
|
}
|
|
31
48
|
}
|
|
@@ -57,36 +57,40 @@ export const DevPagesPanel = () => {
|
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
59
|
<View gap="m">
|
|
60
|
-
<Text variant="
|
|
60
|
+
<Text variant="heading-tertiary" text="design-system.dev.pagesPanel.title" />
|
|
61
61
|
|
|
62
62
|
<View gap="xs" style={currentPageStyles}>
|
|
63
|
-
<Text
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
<Text
|
|
64
|
+
variant="heading-tertiary"
|
|
65
|
+
text="design-system.dev.pagesPanel.currentPage"
|
|
66
|
+
style={{ marginBottom: 'var(--space-xs)' }}
|
|
67
|
+
/>
|
|
66
68
|
<Text style={monoStyle}>
|
|
67
|
-
<strong
|
|
69
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.id" />:</strong> {app?.pageId || '—'}
|
|
68
70
|
</Text>
|
|
69
71
|
<Text style={monoStyle}>
|
|
70
|
-
<strong
|
|
72
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.path" />:</strong> {currentPathLabel}
|
|
71
73
|
</Text>
|
|
72
74
|
<Text style={monoStyle}>
|
|
73
|
-
<strong
|
|
75
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.url" />:</strong> {currentUrl}
|
|
74
76
|
</Text>
|
|
75
77
|
{packageDetailHref && currentPage?.package && (
|
|
76
78
|
<Button
|
|
77
79
|
variant="link"
|
|
78
80
|
href={packageDetailHref}
|
|
81
|
+
label="design-system.dev.pagesPanel.viewPackage"
|
|
82
|
+
params={{ package: currentPage.package }}
|
|
79
83
|
style={{ alignSelf: 'flex-start', padding: 0, height: 'auto' }}
|
|
80
|
-
|
|
81
|
-
View package ({currentPage.package})
|
|
82
|
-
</Button>
|
|
84
|
+
/>
|
|
83
85
|
)}
|
|
84
86
|
</View>
|
|
85
87
|
|
|
86
88
|
<View gap="s">
|
|
87
|
-
<Text
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
<Text
|
|
90
|
+
variant="heading-tertiary"
|
|
91
|
+
text="design-system.dev.pagesPanel.pages"
|
|
92
|
+
params={{ count: pages.length }}
|
|
93
|
+
/>
|
|
90
94
|
|
|
91
95
|
{groups.map(({ key, label, pages: groupPages }) => (
|
|
92
96
|
<View key={key} gap="xs">
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Button, useLocale } from '@ossy/design-system'
|
|
3
|
+
import { useRouter } from '@ossy/router-react'
|
|
4
|
+
import { OpenSignIn, OpenSignUp } from '@ossy/authentication'
|
|
5
|
+
import { useApp } from './AppContext.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Profile link when authenticated; sign-in / sign-up when not.
|
|
9
|
+
*/
|
|
10
|
+
export function HeaderAuthActions ({ compact = false }) {
|
|
11
|
+
const app = useApp()
|
|
12
|
+
const router = useRouter()
|
|
13
|
+
const { t } = useLocale()
|
|
14
|
+
|
|
15
|
+
if (app?.isAuthenticated) {
|
|
16
|
+
return (
|
|
17
|
+
<Button
|
|
18
|
+
variant="link"
|
|
19
|
+
suffix="profile"
|
|
20
|
+
href={router.getHref('@profile')}
|
|
21
|
+
aria-label={t('app.shell.header.profile') || 'Profile'}
|
|
22
|
+
style={{ flexShrink: 0 }}
|
|
23
|
+
>
|
|
24
|
+
{compact ? null : (t('app.shell.header.profile') || 'Profile')}
|
|
25
|
+
</Button>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Button
|
|
32
|
+
variant="link"
|
|
33
|
+
suffix={OpenSignIn.suffix}
|
|
34
|
+
href={router.getHref('@sign-in')}
|
|
35
|
+
label={compact ? undefined : OpenSignIn.label}
|
|
36
|
+
aria-label={t(OpenSignIn.label)}
|
|
37
|
+
style={{ flexShrink: 0 }}
|
|
38
|
+
/>
|
|
39
|
+
<Button
|
|
40
|
+
variant="cta"
|
|
41
|
+
suffix={OpenSignUp.suffix}
|
|
42
|
+
href={router.getHref('@sign-up')}
|
|
43
|
+
label={compact ? undefined : OpenSignUp.label}
|
|
44
|
+
aria-label={t(OpenSignUp.label)}
|
|
45
|
+
style={{ flexShrink: 0 }}
|
|
46
|
+
/>
|
|
47
|
+
</>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { Text, View, useLocale } from '@ossy/design-system'
|
|
3
|
+
import { useRouter } from '@ossy/router-react'
|
|
4
|
+
import { languageDisplayName } from './languageCode.js'
|
|
5
|
+
|
|
6
|
+
const listItemStyle = {
|
|
7
|
+
display: 'block',
|
|
8
|
+
width: '100%',
|
|
9
|
+
textAlign: 'left',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Footer language column when multiple locales are configured.
|
|
14
|
+
*/
|
|
15
|
+
export function LanguageList () {
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const { t } = useLocale()
|
|
18
|
+
const { language, supportedLanguages, getHref } = router
|
|
19
|
+
|
|
20
|
+
const displayNames = useMemo(
|
|
21
|
+
() => new Intl.DisplayNames([language], { type: 'language' }),
|
|
22
|
+
[language],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if (!supportedLanguages?.length || supportedLanguages.length <= 1) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listLabel = t('app.shell.footer.languages') || 'Languages'
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View layout="column" gap="s" alignItems="flex-start" style={{ minWidth: 0 }}>
|
|
33
|
+
<Text variant="heading-tertiary" as="h2" style={listItemStyle}>
|
|
34
|
+
{listLabel}
|
|
35
|
+
</Text>
|
|
36
|
+
<View
|
|
37
|
+
as="nav"
|
|
38
|
+
aria-label={listLabel}
|
|
39
|
+
style={{ width: '100%' }}
|
|
40
|
+
>
|
|
41
|
+
<View
|
|
42
|
+
as="ul"
|
|
43
|
+
layout="column"
|
|
44
|
+
gap="xs"
|
|
45
|
+
alignItems="flex-start"
|
|
46
|
+
style={{ listStyle: 'none', margin: 0, padding: 0, width: '100%' }}
|
|
47
|
+
>
|
|
48
|
+
{supportedLanguages.map((lang) => {
|
|
49
|
+
const isActive = lang === language
|
|
50
|
+
const label = displayNames.of(lang) ?? languageDisplayName(lang, language)
|
|
51
|
+
|
|
52
|
+
if (isActive) {
|
|
53
|
+
return (
|
|
54
|
+
<View as="li" key={lang} style={{ width: '100%' }}>
|
|
55
|
+
<Text variant="small" as="span" aria-current="true" style={listItemStyle}>
|
|
56
|
+
{label}
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View as="li" key={lang} style={{ width: '100%' }}>
|
|
64
|
+
<Text
|
|
65
|
+
variant="small"
|
|
66
|
+
as="a"
|
|
67
|
+
href={getHref({ language: lang })}
|
|
68
|
+
style={{
|
|
69
|
+
...listItemStyle,
|
|
70
|
+
textDecoration: 'underline',
|
|
71
|
+
cursor: 'pointer',
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{label}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
</View>
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
)
|
|
83
|
+
}
|