openxiangda-cli 2.0.0-alpha.150 → 2.0.0-alpha.153
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/package.json +5 -5
- package/template/AGENTS.md +1 -1
- package/template/README.md +2 -2
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +12 -65
- package/template/apps/web/e2e/workflow.spec.ts +180 -51
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/main.tsx +2 -0
- package/template/apps/web/test/contracts.test.ts +8 -2
- package/template/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.153",
|
|
4
4
|
"description": "Thin application-level CLI for OpenXiangda 2.0.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@oclif/core": "4.13.3",
|
|
24
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
25
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-mcp": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-skill-kit": "2.0.0-alpha.
|
|
24
|
+
"openxiangda-contracts": "2.0.0-alpha.80",
|
|
25
|
+
"openxiangda-devkit-core": "2.0.0-alpha.100",
|
|
26
|
+
"openxiangda-mcp": "2.0.0-alpha.100",
|
|
27
|
+
"openxiangda-skill-kit": "2.0.0-alpha.122"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "4.23.12",
|
package/template/AGENTS.md
CHANGED
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
- `option.*`, `user.*`, `department.*` and `resource-ref.*` values never collapse to scalar IDs. Single values store one labeled object and multiple values store object arrays. `resource-ref.*` JSON is not a foreign key, trusted target snapshot or automatically refreshed copy; current target state is read by `resourceCode` plus `value`. A resource source `labelField` must be `text.short` or `text.long`; `serial-number` is allowed in source search, description and snapshot fields, but not as the label. `location` accepts only exact WGS84 coordinates captured by DingTalk or browser geolocation; it has no manual input or `manual` source. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
|
|
43
43
|
- Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
|
|
44
44
|
- Do not add compatibility aliases, migration branches or silent fallbacks for an earlier 2.0 alpha contract. Replace an incorrect contract and regenerate the application.
|
|
45
|
-
- Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through canonical `openxiangda.config.ts` declarations and generated clients. `standalone`/`hidden-handoff`
|
|
45
|
+
- Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through canonical `openxiangda.config.ts` declarations and generated clients. `standalone`/`hidden-handoff` default to the compiler-owned process operation; action-owned submission must instead declare `launch.submission.kind: 'named-operation'` with sealed create/existing input and output bindings so the standard PC/mobile page calls the original Named Action and accepts an explicit no-Workflow result. Never add a browser save callback or call Workflow prepare/start directly. Custom pages launch only through a verified Named Action using `OpenXiangdaBusinessProcessService`. PC and mobile render the same `ProcessCommandSurface` independently and recover only by `commandId`. To replace Workflow detail, declare both desktop and mobile `detailRouteCode` values whose routes contain exactly `:instanceId`. Add the current-user todo page only with `frontend.user.applicationTodoCenter: true`; never query Notification Hub management APIs from a user page.
|
|
46
46
|
- Run `pnpm openxiangda check --json` after contract changes and read `data.sealedArtifact`; check never seals, and an older `.openxiangda/build/app-package.json` is not the current check result. Use `pnpm openxiangda accept --plan <file>` only for optional real preproduction identity acceptance; it never blocks delivery. Deploy with `pnpm openxiangda deploy`, inspect with `pnpm openxiangda status` and `pnpm openxiangda logs`, and use the platform rollback command rather than mutating K3s directly.
|
|
47
47
|
- AI-native clients start the workspace protocol through the same pinned binary: `pnpm exec openxiangda --mcp-stdio --cwd <workspace>`. Read `openxiangda://workspace/contracts` or call `contract_describe`; for first-time menu authoring, copy `data.adminNavigationAuthoring.suggestion.expression` once into `frontend.admin.navigation` with its listed `openxiangda/config` imports, then edit that application-owned declaration. Never treat the proposal as runtime discovery. When AppSpec is enabled, also read `openxiangda://workspace/appspec` or call `appspec_context`. Require `aiCatalog` and `aiCatalogDigest` to match the normal compiler output. This is a stdio transport mode, not another application-owned server. Never write an application MCP server, Catalog file, preview store or second authorization path.
|
package/template/README.md
CHANGED
|
@@ -23,8 +23,8 @@ pnpm openxiangda dev
|
|
|
23
23
|
逐项绑定本地组件并传给 `OpenXiangdaApplication`。后者是唯一的
|
|
24
24
|
BrowserRouter、RuntimeBoundary、Refine、generated routes 和后台 Shell
|
|
25
25
|
所有者;用户页可以拥有独立布局,但不要再建 router/Shell。要使用通用应用待办中心,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
声明 `frontend.user.applicationTodoCenter: true`;平台生成 `/todos` 与 `/m/todos`,
|
|
27
|
+
只读取当前用户的消息/待办投影并统一解析目标页面。
|
|
28
28
|
|
|
29
29
|
自定义后台页使用 `OpenXiangdaAdminPage` 作为页面根;Ant Design 组件自动继承平台
|
|
30
30
|
`ConfigProvider`,图表等 TypeScript 渲染值读取 `theme.useToken()`,自定义 CSS
|
|
@@ -90,44 +90,7 @@ const applicationContributions = defineApplicationContributions(detailRoutes, {
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
const adminPages = [
|
|
94
|
-
{
|
|
95
|
-
code: 'application:todo-center',
|
|
96
|
-
kind: 'application-todo-center',
|
|
97
|
-
path: '/admin/todos',
|
|
98
|
-
label: '待办中心',
|
|
99
|
-
navigationEligible: true,
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
code: 'workflow:work-center',
|
|
103
|
-
kind: 'workflow-work-center',
|
|
104
|
-
path: '/admin/work-center',
|
|
105
|
-
label: '我的审批',
|
|
106
|
-
navigationEligible: true,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
code: 'workflow:purchase-approval:launch',
|
|
110
|
-
kind: 'workflow-launch',
|
|
111
|
-
path: '/admin/workflows/purchase-approval/new',
|
|
112
|
-
label: '发起采购审批',
|
|
113
|
-
navigationEligible: true,
|
|
114
|
-
workflowCode: 'purchase-approval',
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
code: 'workflow:task',
|
|
118
|
-
kind: 'workflow-task',
|
|
119
|
-
path: '/tasks/:taskId',
|
|
120
|
-
label: '审批任务',
|
|
121
|
-
navigationEligible: false,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
code: 'workflow:instance',
|
|
125
|
-
kind: 'workflow-instance',
|
|
126
|
-
path: '/workflows/:instanceId',
|
|
127
|
-
label: '流程详情',
|
|
128
|
-
navigationEligible: false,
|
|
129
|
-
},
|
|
130
|
-
] as const satisfies AdminPagesInput;
|
|
93
|
+
const adminPages = [] as const satisfies AdminPagesInput;
|
|
131
94
|
|
|
132
95
|
const routeManifest = {
|
|
133
96
|
schemaVersion: 'openxiangda.application-route-manifest/v3',
|
|
@@ -148,8 +111,8 @@ const routeManifest = {
|
|
|
148
111
|
kind: 'application-todo-center',
|
|
149
112
|
desktop: {
|
|
150
113
|
routeCode: 'application.todo-center.desktop',
|
|
151
|
-
path: '/
|
|
152
|
-
surface: '
|
|
114
|
+
path: '/todos',
|
|
115
|
+
surface: 'user',
|
|
153
116
|
pathParams: [],
|
|
154
117
|
requiresAuthentication: true,
|
|
155
118
|
},
|
|
@@ -166,8 +129,8 @@ const routeManifest = {
|
|
|
166
129
|
kind: 'workflow-work-center',
|
|
167
130
|
desktop: {
|
|
168
131
|
routeCode: 'workflow.work.center.desktop',
|
|
169
|
-
path: '/
|
|
170
|
-
surface: '
|
|
132
|
+
path: '/work-center',
|
|
133
|
+
surface: 'user',
|
|
171
134
|
pathParams: [],
|
|
172
135
|
requiresAuthentication: true,
|
|
173
136
|
},
|
|
@@ -185,7 +148,7 @@ const routeManifest = {
|
|
|
185
148
|
desktop: {
|
|
186
149
|
routeCode: 'workflow.task.desktop',
|
|
187
150
|
path: '/tasks/:taskId',
|
|
188
|
-
surface: '
|
|
151
|
+
surface: 'user',
|
|
189
152
|
pathParams: ['taskId'],
|
|
190
153
|
requiresAuthentication: true,
|
|
191
154
|
},
|
|
@@ -203,7 +166,7 @@ const routeManifest = {
|
|
|
203
166
|
desktop: {
|
|
204
167
|
routeCode: 'workflow.instance.desktop',
|
|
205
168
|
path: '/workflows/:instanceId',
|
|
206
|
-
surface: '
|
|
169
|
+
surface: 'user',
|
|
207
170
|
pathParams: ['instanceId'],
|
|
208
171
|
requiresAuthentication: true,
|
|
209
172
|
},
|
|
@@ -221,9 +184,9 @@ const routeManifest = {
|
|
|
221
184
|
workflowCode: 'purchase-approval',
|
|
222
185
|
desktop: {
|
|
223
186
|
routeCode: 'workflow.purchase-approval.launch.desktop',
|
|
224
|
-
path: '/
|
|
225
|
-
surface: '
|
|
226
|
-
pathParams: [],
|
|
187
|
+
path: '/workflows/:workflowCode/start',
|
|
188
|
+
surface: 'user',
|
|
189
|
+
pathParams: ['workflowCode'],
|
|
227
190
|
requiresAuthentication: true,
|
|
228
191
|
},
|
|
229
192
|
mobile: {
|
|
@@ -238,26 +201,10 @@ const routeManifest = {
|
|
|
238
201
|
digest: '34cc08a02cd187a07d9e2d2f99c1080b5b86f3a4512f9e0f43ff4cb05b7195fa',
|
|
239
202
|
} as const;
|
|
240
203
|
|
|
241
|
-
const adminNavigation = [
|
|
242
|
-
{
|
|
243
|
-
code: 'workflow-center',
|
|
244
|
-
label: '流程中心',
|
|
245
|
-
icon: 'workflow',
|
|
246
|
-
order: 0,
|
|
247
|
-
items: [
|
|
248
|
-
{ pageCode: 'application:todo-center', label: '待办中心', order: 0 },
|
|
249
|
-
{ pageCode: 'workflow:work-center', label: '我的审批', order: 1 },
|
|
250
|
-
{
|
|
251
|
-
pageCode: 'workflow:purchase-approval:launch',
|
|
252
|
-
label: '发起采购审批',
|
|
253
|
-
order: 2,
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
},
|
|
257
|
-
] as const satisfies AdminNavigationInput;
|
|
204
|
+
const adminNavigation = [] as const satisfies AdminNavigationInput;
|
|
258
205
|
|
|
259
206
|
const fixtureParams = new URLSearchParams(window.location.search);
|
|
260
|
-
const initialPath = fixtureParams.get('initial') || '/
|
|
207
|
+
const initialPath = fixtureParams.get('initial') || '/work-center';
|
|
261
208
|
const runtimeBase = fixtureParams.get('base')?.trim();
|
|
262
209
|
if (runtimeBase) {
|
|
263
210
|
for (const [name, content] of [
|
|
@@ -62,7 +62,7 @@ function surface(completed = false, customDetail = false) {
|
|
|
62
62
|
return {
|
|
63
63
|
schemaVersion: 'openxiangda.workflow-surface/v2',
|
|
64
64
|
protocolVersion: 'workflow_surface_v2',
|
|
65
|
-
surfaceRevision: completed ? '
|
|
65
|
+
surfaceRevision: (completed ? 'b' : 'a').repeat(64),
|
|
66
66
|
engineVersion: '2.0',
|
|
67
67
|
commandToken: 'A'.repeat(43),
|
|
68
68
|
commandTokenExpiresAt: '2099-08-29T09:05:00.000Z',
|
|
@@ -196,7 +196,7 @@ function surface(completed = false, customDetail = false) {
|
|
|
196
196
|
},
|
|
197
197
|
execute: {
|
|
198
198
|
method: 'POST',
|
|
199
|
-
href:
|
|
199
|
+
href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/approve`,
|
|
200
200
|
idempotencyRequired: true,
|
|
201
201
|
},
|
|
202
202
|
refresh: ['surface', 'timeline', 'work_center'],
|
|
@@ -226,7 +226,7 @@ function surface(completed = false, customDetail = false) {
|
|
|
226
226
|
uiSchema: { confirmText: `确认${key}` },
|
|
227
227
|
execute: {
|
|
228
228
|
method: 'POST',
|
|
229
|
-
href: `/workflow/${key}`,
|
|
229
|
+
href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/${key}`,
|
|
230
230
|
idempotencyRequired: true,
|
|
231
231
|
},
|
|
232
232
|
refresh: ['surface', 'timeline', 'work_center'],
|
|
@@ -237,74 +237,180 @@ function surface(completed = false, customDetail = false) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function timeline(completed = false) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
const flow = [
|
|
241
|
+
{
|
|
242
|
+
key: 'start',
|
|
243
|
+
nodeId: null,
|
|
244
|
+
kind: 'start',
|
|
245
|
+
title: '张三',
|
|
246
|
+
status: 'completed',
|
|
247
|
+
startedAt: '2026-08-25T01:00:00.000Z',
|
|
248
|
+
completedAt: '2026-08-25T01:00:00.000Z',
|
|
249
|
+
assignees: [],
|
|
250
|
+
operations: [],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
key: 'manager',
|
|
254
|
+
nodeId: 'manager-review',
|
|
255
|
+
kind: 'approval',
|
|
256
|
+
title: '部门负责人',
|
|
257
|
+
status: completed ? 'completed' : 'active',
|
|
258
|
+
startedAt: '2026-08-25T01:01:00.000Z',
|
|
259
|
+
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
260
|
+
assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
|
|
261
|
+
operations: [{
|
|
262
|
+
id: 'operation-transfer',
|
|
263
|
+
operation: 'transfer',
|
|
264
|
+
operationLabel: '转交',
|
|
265
|
+
actorUserId: 'acceptance-user',
|
|
266
|
+
actorDisplayName: '李明',
|
|
267
|
+
actingForUserId: null,
|
|
268
|
+
reason: '由值班负责人继续处理',
|
|
269
|
+
detail: {},
|
|
270
|
+
severity: 'normal',
|
|
271
|
+
createdAt: '2026-08-25T01:08:00.000Z',
|
|
272
|
+
}],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
key: 'finance',
|
|
276
|
+
nodeId: 'finance-review',
|
|
277
|
+
kind: 'approval',
|
|
278
|
+
title: '财务负责人',
|
|
279
|
+
status: completed ? 'completed' : 'waiting',
|
|
280
|
+
startedAt: null,
|
|
281
|
+
completedAt: null,
|
|
282
|
+
assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
|
|
283
|
+
operations: [],
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
const items = [{
|
|
287
|
+
id: 'operation-transfer',
|
|
288
|
+
operation: 'transfer',
|
|
289
|
+
operationLabel: '转交',
|
|
290
|
+
actorUserId: 'acceptance-user',
|
|
291
|
+
actorDisplayName: '李明',
|
|
292
|
+
actingForUserId: null,
|
|
293
|
+
reason: '由值班负责人继续处理',
|
|
294
|
+
detail: {},
|
|
295
|
+
severity: 'normal',
|
|
296
|
+
createdAt: '2026-08-25T01:08:00.000Z',
|
|
297
|
+
}];
|
|
298
|
+
const display = {
|
|
299
|
+
entries: [
|
|
244
300
|
{
|
|
245
301
|
key: 'start',
|
|
302
|
+
kind: 'submission',
|
|
246
303
|
nodeId: null,
|
|
247
|
-
|
|
248
|
-
title: '
|
|
304
|
+
nodeKind: 'start',
|
|
305
|
+
title: '提交申请',
|
|
249
306
|
status: 'completed',
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
enteredAt: '2026-08-25T01:00:00.000Z',
|
|
308
|
+
leftAt: '2026-08-25T01:00:00.000Z',
|
|
309
|
+
primaryDisplayTime: '2026-08-25T01:00:00.000Z',
|
|
310
|
+
people: [{
|
|
311
|
+
userId: 'applicant-user',
|
|
312
|
+
displayName: '张三',
|
|
313
|
+
avatarUrl: null,
|
|
314
|
+
departmentDisplayName: '理学院',
|
|
315
|
+
}],
|
|
253
316
|
operations: [],
|
|
317
|
+
result: {},
|
|
318
|
+
terminalReason: null,
|
|
254
319
|
},
|
|
255
320
|
{
|
|
256
321
|
key: 'manager',
|
|
322
|
+
kind: 'node',
|
|
257
323
|
nodeId: 'manager-review',
|
|
258
|
-
|
|
324
|
+
nodeKind: 'approval',
|
|
259
325
|
title: '部门负责人',
|
|
260
326
|
status: completed ? 'completed' : 'active',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
327
|
+
enteredAt: '2026-08-25T01:01:00.000Z',
|
|
328
|
+
leftAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
329
|
+
primaryDisplayTime: '2026-08-25T01:08:00.000Z',
|
|
330
|
+
people: [],
|
|
264
331
|
operations: [{
|
|
265
332
|
id: 'operation-transfer',
|
|
266
333
|
operation: 'transfer',
|
|
267
334
|
operationLabel: '转交',
|
|
268
|
-
|
|
269
|
-
|
|
335
|
+
actor: {
|
|
336
|
+
userId: 'acceptance-user',
|
|
337
|
+
displayName: '李明',
|
|
338
|
+
avatarUrl: null,
|
|
339
|
+
departmentDisplayName: null,
|
|
340
|
+
},
|
|
270
341
|
actingForUserId: null,
|
|
271
342
|
reason: '由值班负责人继续处理',
|
|
272
|
-
detail: {},
|
|
273
343
|
severity: 'normal',
|
|
274
|
-
|
|
344
|
+
occurredAt: '2026-08-25T01:08:00.000Z',
|
|
275
345
|
}],
|
|
346
|
+
result: {},
|
|
347
|
+
terminalReason: null,
|
|
276
348
|
},
|
|
277
349
|
{
|
|
278
350
|
key: 'finance',
|
|
351
|
+
kind: completed ? 'node' : 'planned_node',
|
|
279
352
|
nodeId: 'finance-review',
|
|
280
|
-
|
|
353
|
+
nodeKind: 'approval',
|
|
281
354
|
title: '财务负责人',
|
|
282
355
|
status: completed ? 'completed' : 'waiting',
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
356
|
+
enteredAt: null,
|
|
357
|
+
leftAt: null,
|
|
358
|
+
primaryDisplayTime: null,
|
|
359
|
+
people: [{
|
|
360
|
+
userId: 'finance-user',
|
|
361
|
+
displayName: '财务负责人',
|
|
362
|
+
avatarUrl: null,
|
|
363
|
+
departmentDisplayName: null,
|
|
364
|
+
}],
|
|
286
365
|
operations: [],
|
|
366
|
+
result: {},
|
|
367
|
+
terminalReason: null,
|
|
287
368
|
},
|
|
288
369
|
],
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
engineVersion: '2.0',
|
|
373
|
+
instanceId,
|
|
374
|
+
instanceSequence: completed ? 4 : 3,
|
|
375
|
+
timelineRevision: (completed ? 'f' : 'e').repeat(64),
|
|
376
|
+
flow,
|
|
377
|
+
items,
|
|
378
|
+
display,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function detail(completed = false, customDetail = false) {
|
|
383
|
+
const currentSurface = surface(completed, customDetail);
|
|
384
|
+
const currentTimeline = timeline(completed);
|
|
385
|
+
return {
|
|
386
|
+
schemaVersion: 'openxiangda.workflow-detail-surface/v2',
|
|
387
|
+
protocolVersion: 'workflow_detail_surface_v2',
|
|
388
|
+
detailRevision: (completed ? '2' : '1').repeat(64),
|
|
389
|
+
instanceSequence: currentSurface.instanceSequence,
|
|
390
|
+
timelineRevision: currentTimeline.timelineRevision,
|
|
391
|
+
surface: currentSurface,
|
|
392
|
+
timeline: currentTimeline,
|
|
393
|
+
currentStatus: {
|
|
394
|
+
code: completed ? 'approved' : 'assigned',
|
|
395
|
+
label: completed ? '已同意' : '待审批',
|
|
396
|
+
tone: completed ? 'success' : 'warning',
|
|
397
|
+
},
|
|
398
|
+
nodes: currentTimeline.display.entries,
|
|
399
|
+
handlingRecords: currentTimeline.display.entries.flatMap(
|
|
400
|
+
entry => entry.operations
|
|
401
|
+
),
|
|
402
|
+
operations: currentSurface.operations,
|
|
403
|
+
navigationContext: {
|
|
404
|
+
desktopReturnPath: '/admin/work-center',
|
|
405
|
+
mobileReturnPath: '/m/work-center',
|
|
406
|
+
},
|
|
301
407
|
};
|
|
302
408
|
}
|
|
303
409
|
|
|
304
410
|
function launchSurface() {
|
|
305
411
|
return {
|
|
306
|
-
schemaVersion: 'openxiangda.workflow-launch-surface/
|
|
307
|
-
protocolVersion: '
|
|
412
|
+
schemaVersion: 'openxiangda.workflow-launch-surface/v3',
|
|
413
|
+
protocolVersion: 'workflow_launch_surface_v3',
|
|
308
414
|
surfaceRevision: 'launch-revision-1',
|
|
309
415
|
engineVersion: '2.0',
|
|
310
416
|
appCode,
|
|
@@ -328,7 +434,7 @@ function launchSurface() {
|
|
|
328
434
|
contractRevisionId: 'contract-revision-1',
|
|
329
435
|
},
|
|
330
436
|
paths: {
|
|
331
|
-
desktop: '/
|
|
437
|
+
desktop: '/workflows/purchase-approval/start',
|
|
332
438
|
mobile: '/m/workflows/purchase-approval/start',
|
|
333
439
|
},
|
|
334
440
|
commit: {
|
|
@@ -336,6 +442,15 @@ function launchSurface() {
|
|
|
336
442
|
href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
|
|
337
443
|
idempotencyRequired: true,
|
|
338
444
|
},
|
|
445
|
+
submission: {
|
|
446
|
+
kind: 'standard-process',
|
|
447
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
448
|
+
commit: {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
|
|
451
|
+
idempotencyRequired: true,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
339
454
|
};
|
|
340
455
|
}
|
|
341
456
|
|
|
@@ -610,6 +725,20 @@ async function mockWorkflow(
|
|
|
610
725
|
),
|
|
611
726
|
});
|
|
612
727
|
}
|
|
728
|
+
if (
|
|
729
|
+
path.endsWith(`/workflow/tasks/${taskId}/detail`) ||
|
|
730
|
+
path.endsWith(`/workflow/instances/${instanceId}/detail`)
|
|
731
|
+
) {
|
|
732
|
+
expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
|
|
733
|
+
'workflow-csrf-1'
|
|
734
|
+
);
|
|
735
|
+
return route.fulfill({
|
|
736
|
+
contentType: 'application/json',
|
|
737
|
+
body: JSON.stringify(
|
|
738
|
+
envelope(detail(completed, options.customDetail))
|
|
739
|
+
),
|
|
740
|
+
});
|
|
741
|
+
}
|
|
613
742
|
if (path.endsWith(`/workflow/instances/${instanceId}/surface`)) {
|
|
614
743
|
return route.fulfill({
|
|
615
744
|
contentType: 'application/json',
|
|
@@ -665,7 +794,7 @@ test('renders a paged desktop work center from the current-user role union', asy
|
|
|
665
794
|
page,
|
|
666
795
|
}) => {
|
|
667
796
|
await mockWorkflow(page);
|
|
668
|
-
await page.goto('/workflow-experience.e2e.html?initial=/
|
|
797
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center');
|
|
669
798
|
await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
|
|
670
799
|
await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
|
|
671
800
|
await expect(page.getByText('PO-2026-0825')).toBeVisible();
|
|
@@ -675,7 +804,7 @@ test('renders the generic desktop and mobile application todo center without a s
|
|
|
675
804
|
page,
|
|
676
805
|
}) => {
|
|
677
806
|
await mockWorkflow(page);
|
|
678
|
-
await page.goto('/workflow-experience.e2e.html?initial=/
|
|
807
|
+
await page.goto('/workflow-experience.e2e.html?initial=/todos');
|
|
679
808
|
await expect(page.getByRole('heading', { name: '待办中心' })).toBeVisible();
|
|
680
809
|
await expect(page.locator('.oxa-todo-table')).toBeVisible();
|
|
681
810
|
await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
|
|
@@ -733,7 +862,7 @@ test('routes desktop and mobile work-center entries to the declared custom detai
|
|
|
733
862
|
page,
|
|
734
863
|
}) => {
|
|
735
864
|
await mockWorkflow(page, { customDetail: true });
|
|
736
|
-
await page.goto('/workflow-experience.e2e.html?initial=/
|
|
865
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center');
|
|
737
866
|
await page.getByRole('link', { name: '采购申请审批' }).click();
|
|
738
867
|
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
739
868
|
`${instanceId} / ${taskId}`
|
|
@@ -765,16 +894,16 @@ test('redirects a direct standard task URL to the declared custom detail page',
|
|
|
765
894
|
);
|
|
766
895
|
});
|
|
767
896
|
|
|
768
|
-
test('renders a standalone desktop detail
|
|
897
|
+
test('renders a standalone desktop detail on the canonical user URL', async ({
|
|
769
898
|
page,
|
|
770
899
|
}) => {
|
|
771
900
|
await mockWorkflow(page);
|
|
772
901
|
await page.goto(
|
|
773
|
-
workflowFixtureUrl(`/
|
|
902
|
+
workflowFixtureUrl(`/tasks/${taskId}?from=todo#approval`),
|
|
774
903
|
);
|
|
775
904
|
await expect(page.locator('.oxa-workflow-desktop-standalone-page')).toBeVisible();
|
|
776
905
|
await expect(page).toHaveURL(
|
|
777
|
-
new RegExp(`/tasks/${taskId}\\?from=
|
|
906
|
+
new RegExp(`/tasks/${taskId}\\?from=todo#approval$`),
|
|
778
907
|
);
|
|
779
908
|
await expect(page.locator('.oxa-app-layout')).toHaveCount(0);
|
|
780
909
|
await expect(page.locator('.oxa-topbar')).toHaveCount(0);
|
|
@@ -822,7 +951,7 @@ test('shows an explicit authorization error instead of an empty work center', as
|
|
|
822
951
|
}) => {
|
|
823
952
|
test.setTimeout(45_000);
|
|
824
953
|
await mockWorkflow(page, { forbidden: true });
|
|
825
|
-
await page.goto('/workflow-experience.e2e.html?initial=/
|
|
954
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center', {
|
|
826
955
|
waitUntil: 'domcontentloaded',
|
|
827
956
|
});
|
|
828
957
|
await expect(page.getByRole('alert')).toContainText('403: forbidden', {
|
|
@@ -837,7 +966,7 @@ test('negotiates direct standard routes after BrowserRouter listener setup', asy
|
|
|
837
966
|
await mockWorkflow(page);
|
|
838
967
|
await page.setViewportSize({ width: 360, height: 844 });
|
|
839
968
|
await page.goto(
|
|
840
|
-
workflowFixtureUrl('/
|
|
969
|
+
workflowFixtureUrl('/todos?view=pending#unread', {
|
|
841
970
|
state: 'deep-link',
|
|
842
971
|
}),
|
|
843
972
|
);
|
|
@@ -862,13 +991,13 @@ test('negotiates direct standard routes after BrowserRouter listener setup', asy
|
|
|
862
991
|
await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
|
|
863
992
|
await page.setViewportSize({ width: 901, height: 844 });
|
|
864
993
|
await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
|
|
865
|
-
await expect(page).toHaveURL(/\/
|
|
994
|
+
await expect(page).toHaveURL(/\/todos\?view=pending#unread$/);
|
|
866
995
|
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
|
|
867
996
|
expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
|
|
868
997
|
routeNegotiationState: 'deep-link',
|
|
869
998
|
});
|
|
870
999
|
await page.goBack();
|
|
871
|
-
await expect(page).not.toHaveURL(/\/
|
|
1000
|
+
await expect(page).not.toHaveURL(/\/todos\?view=pending#unread$/);
|
|
872
1001
|
|
|
873
1002
|
// Direct task and instance URLs carry their dynamic parameter and deep-link
|
|
874
1003
|
// context to the independent mobile renderer at the narrow width.
|
|
@@ -898,7 +1027,7 @@ test('negotiates launch pairs in both directions and preserves the mount basenam
|
|
|
898
1027
|
await page.setViewportSize({ width: 360, height: 844 });
|
|
899
1028
|
await page.goto(
|
|
900
1029
|
workflowFixtureUrl(
|
|
901
|
-
`${base}/
|
|
1030
|
+
`${base}/workflows/purchase-approval/start?source=todo#launch`,
|
|
902
1031
|
{ base },
|
|
903
1032
|
),
|
|
904
1033
|
);
|
|
@@ -913,7 +1042,7 @@ test('negotiates launch pairs in both directions and preserves the mount basenam
|
|
|
913
1042
|
await expect(page.locator('.oxa-workflow-submission-desktop')).toBeVisible();
|
|
914
1043
|
await expect(page).toHaveURL(
|
|
915
1044
|
new RegExp(
|
|
916
|
-
`${base}/
|
|
1045
|
+
`${base}/workflows/purchase-approval/start\\?source=todo#launch$`,
|
|
917
1046
|
),
|
|
918
1047
|
);
|
|
919
1048
|
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
authenticationSurfaces,
|
|
17
17
|
anonymousPublicAccess,
|
|
18
18
|
adminNavigation,
|
|
19
|
+
adminAccess,
|
|
19
20
|
adminPages,
|
|
20
21
|
resourceDefinitions,
|
|
21
22
|
workflowDefinitions,
|
|
@@ -50,6 +51,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
50
51
|
<OpenXiangdaApplication
|
|
51
52
|
appCode={appCode}
|
|
52
53
|
appName={appName}
|
|
54
|
+
adminAccess={adminAccess}
|
|
53
55
|
adminNavigation={adminNavigation}
|
|
54
56
|
adminPages={adminPages}
|
|
55
57
|
routeManifest={routeManifest}
|
|
@@ -102,7 +102,7 @@ test('published E2E harnesses do not write evidence into the platform source rep
|
|
|
102
102
|
assert.doesNotMatch(workflowSource, /docs\/architecture|evidenceDirectory/);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('E2E experience fixtures explicitly own every tested
|
|
105
|
+
test('E2E experience fixtures explicitly own every tested route', () => {
|
|
106
106
|
const resourceFixture = readFileSync(
|
|
107
107
|
new URL('../e2e/resource-experience-fixture.tsx', import.meta.url),
|
|
108
108
|
'utf8'
|
|
@@ -121,7 +121,13 @@ test('E2E experience fixtures explicitly own every tested admin route', () => {
|
|
|
121
121
|
assert.match(workflowFixture, /code: 'workflow:work-center'/);
|
|
122
122
|
assert.match(workflowFixture, /code: 'workflow:task'/);
|
|
123
123
|
assert.match(workflowFixture, /code: 'workflow:instance'/);
|
|
124
|
-
assert.match(workflowFixture, /
|
|
124
|
+
assert.match(workflowFixture, /path: '\/work-center'/);
|
|
125
|
+
assert.match(workflowFixture, /path: '\/tasks\/:taskId'/);
|
|
126
|
+
assert.match(workflowFixture, /path: '\/workflows\/:instanceId'/);
|
|
127
|
+
assert.doesNotMatch(
|
|
128
|
+
workflowFixture,
|
|
129
|
+
/path: '\/(?:m\/)?admin\/(?:todos|work-center|tasks|workflows)/
|
|
130
|
+
);
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
test('browser acceptance serializes the prewarmed Vite HTML entrypoints', () => {
|
package/template/package.json
CHANGED
|
@@ -113,6 +113,7 @@ export const platformAuthManifest = {
|
|
|
113
113
|
}
|
|
114
114
|
]
|
|
115
115
|
} as const;
|
|
116
|
+
export const adminAccess = undefined;
|
|
116
117
|
export const routeManifest = {
|
|
117
118
|
"schemaVersion": "openxiangda.application-route-manifest/v3",
|
|
118
119
|
"appCode": "openxiangda-application",
|
|
@@ -165,6 +166,7 @@ export type ApplicationAuthenticationMethod = (typeof authenticationMethods)[num
|
|
|
165
166
|
export type ApplicationAuthenticationSurface = (typeof authenticationSurfaces)[keyof typeof authenticationSurfaces];
|
|
166
167
|
export type ApplicationAuthenticationSurfaceCode = ApplicationAuthenticationSurface["routeCode"];
|
|
167
168
|
export type AnonymousPublicAccess = typeof anonymousPublicAccess;
|
|
169
|
+
export type AdminAccess = typeof adminAccess;
|
|
168
170
|
export type RouteManifest = typeof routeManifest;
|
|
169
171
|
export type AdminPage = (typeof adminPages)[number];
|
|
170
172
|
export type AdminNavigationGroup = (typeof adminNavigation)[number];
|