openxiangda-cli 2.0.0-alpha.114 → 2.0.0-alpha.117

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 CHANGED
@@ -7,7 +7,10 @@ The CLI disables user, development and JIT plugin loading. A clean build removes
7
7
  AI-native development clients use the same pinned executable in pre-command
8
8
  stdio mode: `pnpm exec openxiangda --mcp-stdio --cwd <workspace>`. They can read
9
9
  `openxiangda://workspace/contracts` or call `contract_describe` to obtain the
10
- compiler-owned `aiCatalog` and `aiCatalogDigest`. This transport does not add a
11
- another executable and applications must not implement a second MCP server.
10
+ compiler-owned `aiCatalog`, `aiCatalogDigest` and the typed copy-once
11
+ `adminNavigationAuthoring.suggestion`. Copy its expression into
12
+ `frontend.admin.navigation`; runtime never applies it automatically. This
13
+ transport does not add another executable and applications must not implement
14
+ a second MCP server.
12
15
 
13
16
  `dev` starts local Web/Nest hot reload and a loopback connected proxy. `deploy` defaults to test; production explicitly reuses a successful test AppVersion. Application binding, initialization, generation, tests and builds are internal stages rather than separate commands.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.114",
3
+ "version": "2.0.0-alpha.117",
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.55",
25
- "openxiangda-devkit-core": "2.0.0-alpha.72",
26
- "openxiangda-mcp": "2.0.0-alpha.72",
27
- "openxiangda-skill-kit": "2.0.0-alpha.90"
24
+ "openxiangda-contracts": "2.0.0-alpha.56",
25
+ "openxiangda-devkit-core": "2.0.0-alpha.73",
26
+ "openxiangda-mcp": "2.0.0-alpha.73",
27
+ "openxiangda-skill-kit": "2.0.0-alpha.92"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsx": "4.23.12",
@@ -3,8 +3,8 @@
3
3
  - Use only the workspace-pinned CLI: `pnpm openxiangda <command>`. Never invoke a bare global `openxiangda` inside a 2.0 task.
4
4
  - `appspec/` is the optional OpenXiangda 2.0 business-intent layer. Before changing observable behavior, read the bounded index with `pnpm openxiangda spec context --json`, then select only the relevant stable ID; use no ChangeSpec for L0 work, one short ChangeSpec for L1, and add permission/rollback/concurrency detail only for L2/L3. Before close, the user—not AI—confirms `currentSpec=merged` or `not-applicable`. AppSpec is advisory and never a release gate. Never read, import or migrate 1.x `openspec/` or SDD.
5
5
  - Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
6
- - Keep generated admin routes and custom actions inside the platform Shell. Bind generated `appRoutes` to local operation pages with `defineAdminContributions`, and use only its typed `toolbar`, `row` and `detail` resource slots. Do not create another router, menu, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API authorization remains server-owned.
7
- - Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case.
6
+ - Keep generated admin routes and custom actions inside the platform Shell. Bind generated `appRoutes` to local operation pages with `defineAdminContributions`, and use only its typed `toolbar`, `row` and `detail` resource slots. Declare the complete editable menu with `defineAdminNavigation` and its page/group helpers; the Shell renders only generated `adminNavigation` references and permissions only filter them. Do not create another router, menu store, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API authorization remains server-owned.
7
+ - Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional `mutationOwner`, generated/list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case. Use `native` for direct Data API mutations, `action`, `readonly` or `workflow` for non-Native ownership; never grant or generate Native mutation for a non-Native owner.
8
8
  - Never write resource-level `schemaVersion`, `appCode`, `schema`, `surface`, `capabilities`, `fieldPolicies` or `platform/data` modules. The compiler derives the strict DataResource, CRUD capabilities, Surface and AI Schema. The application manifest still starts with its one top-level `schemaVersion: 3`.
9
9
  - Ordinary list/get/create/update/delete, filters, export and batch operations use the platform Native Data API. Do not create Function CRUD or NestJS wrappers.
10
10
  - Add NestJS only for a named business action that needs a cross-resource transaction, an invariant or an external side effect.
@@ -20,6 +20,6 @@
20
20
  - `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`.
21
21
  - Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
22
22
  - 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.
23
- - Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through the canonical `openxiangda.config.ts` declarations and generated clients; never copy 1.x workflow/message code, tables, APIs, templates or callbacks into this workspace. Keep ordinary CRUD and application-specific state machines independent of them.
23
+ - Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through the canonical `openxiangda.config.ts` declarations and generated clients; declare each Workflow launch as `standalone`, `custom-page`, `hidden-handoff` or `work-center-only`, put only self-contained standalone launch pages in navigation, and bind durable save handlers with `defineWorkflowLaunchContributions` without repeating generated titles. Never depend on temporary router location state or copy 1.x workflow/message code, tables, APIs, templates or callbacks into this workspace. Keep ordinary CRUD and application-specific state machines independent of them.
24
24
  - 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.
25
- - 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`; 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.
25
+ - 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.
@@ -15,7 +15,7 @@
15
15
  "@nestjs/common": "11.1.29",
16
16
  "@nestjs/core": "11.1.29",
17
17
  "@nestjs/platform-fastify": "11.1.29",
18
- "openxiangda": "2.0.0-alpha.27",
18
+ "openxiangda": "2.0.0-alpha.31",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -35,7 +35,14 @@ export const liveDefinition: GeneratedResourceDefinition = {
35
35
  name: '字段协议记录',
36
36
  capabilities,
37
37
  surface: {
38
- generated: true,
38
+ mutationOwner: 'native',
39
+ generated: {
40
+ list: true,
41
+ detail: true,
42
+ create: true,
43
+ update: true,
44
+ delete: true,
45
+ },
39
46
  fields: {
40
47
  text_short: {
41
48
  label: '标题',
@@ -63,7 +63,14 @@ const fields: DataResourceSurface['fields'] = {
63
63
  };
64
64
 
65
65
  const surface: DataResourceSurface = {
66
- generated: true,
66
+ mutationOwner: 'native',
67
+ generated: {
68
+ list: true,
69
+ detail: true,
70
+ create: true,
71
+ update: true,
72
+ delete: true,
73
+ },
67
74
  fields,
68
75
  list: {
69
76
  defaultPageSize: 20,
@@ -145,7 +152,6 @@ const adminContributions = defineAdminContributions(
145
152
  path: '/admin/operations/content',
146
153
  label: '内容业务操作',
147
154
  surface: 'admin',
148
- navigation: 'menu',
149
155
  capability: 'app:openxiangda-application:operations:read',
150
156
  tabPersistence: 'session',
151
157
  keepAlive: 'none',
@@ -179,6 +185,179 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
179
185
  <OpenXiangdaApplication
180
186
  appCode="openxiangda-application"
181
187
  appName="资源体验验收"
188
+ adminNavigation={[
189
+ {
190
+ code: 'resources',
191
+ label: '数据管理',
192
+ icon: 'database',
193
+ order: 0,
194
+ items: [
195
+ { pageCode: 'resource:resource-01:list', label: '内容栏目', order: 0 },
196
+ { pageCode: 'resource:resource-02:list', label: '内容文章', order: 1 },
197
+ { pageCode: 'resource:resource-03:list', label: '内容专题', order: 2 },
198
+ { pageCode: 'resource:resource-04:list', label: '会员档案', order: 3 },
199
+ { pageCode: 'resource:resource-05:list', label: '门户轮播', order: 4 },
200
+ { pageCode: 'resource:resource-06:list', label: '工会小组', order: 5 },
201
+ { pageCode: 'resource:resource-07:list', label: '场地预约', order: 6 },
202
+ {
203
+ pageCode: 'resource:resource-08:list',
204
+ label: '场地不可用时段',
205
+ order: 7,
206
+ },
207
+ { pageCode: 'resource:resource-09:list', label: '场地档案', order: 8 },
208
+ { pageCode: 'resource:resource-10:list', label: '通知公告', order: 9 },
209
+ { pageCode: 'resource:resource-11:list', label: '活动报名', order: 10 },
210
+ { pageCode: 'resource:resource-12:list', label: '资料下载', order: 11 },
211
+ { pageCode: 'operation:content-operations', order: 12 },
212
+ ],
213
+ },
214
+ ]}
215
+ adminPages={[
216
+ {
217
+ code: 'resource:resource-01:list',
218
+ kind: 'resource-list',
219
+ path: '/resource-01',
220
+ label: '资源 01',
221
+ navigationEligible: true,
222
+ capability: resourceDefinitions['resource-01'].capabilities.read,
223
+ resourceCode: 'resource-01',
224
+ },
225
+ {
226
+ code: 'resource:resource-01:detail',
227
+ kind: 'resource-detail',
228
+ path: '/resource-01/:id',
229
+ label: '资源 01 详情',
230
+ navigationEligible: false,
231
+ capability: resourceDefinitions['resource-01'].capabilities.read,
232
+ resourceCode: 'resource-01',
233
+ },
234
+ {
235
+ code: 'resource:resource-01:create',
236
+ kind: 'resource-create',
237
+ path: '/resource-01/new',
238
+ label: '新增资源 01',
239
+ navigationEligible: false,
240
+ capability: resourceDefinitions['resource-01'].capabilities.create,
241
+ resourceCode: 'resource-01',
242
+ },
243
+ {
244
+ code: 'resource:resource-01:update',
245
+ kind: 'resource-update',
246
+ path: '/resource-01/:id/edit',
247
+ label: '编辑资源 01',
248
+ navigationEligible: false,
249
+ capability: resourceDefinitions['resource-01'].capabilities.update,
250
+ resourceCode: 'resource-01',
251
+ },
252
+ {
253
+ code: 'resource:resource-02:list',
254
+ kind: 'resource-list',
255
+ path: '/resource-02',
256
+ label: '内容文章',
257
+ navigationEligible: true,
258
+ capability: resourceDefinitions['resource-02'].capabilities.read,
259
+ resourceCode: 'resource-02',
260
+ },
261
+ {
262
+ code: 'resource:resource-03:list',
263
+ kind: 'resource-list',
264
+ path: '/resource-03',
265
+ label: '内容专题',
266
+ navigationEligible: true,
267
+ capability: resourceDefinitions['resource-03'].capabilities.read,
268
+ resourceCode: 'resource-03',
269
+ },
270
+ {
271
+ code: 'resource:resource-04:list',
272
+ kind: 'resource-list',
273
+ path: '/resource-04',
274
+ label: '会员档案',
275
+ navigationEligible: true,
276
+ capability: resourceDefinitions['resource-04'].capabilities.read,
277
+ resourceCode: 'resource-04',
278
+ },
279
+ {
280
+ code: 'resource:resource-05:list',
281
+ kind: 'resource-list',
282
+ path: '/resource-05',
283
+ label: '门户轮播',
284
+ navigationEligible: true,
285
+ capability: resourceDefinitions['resource-05'].capabilities.read,
286
+ resourceCode: 'resource-05',
287
+ },
288
+ {
289
+ code: 'resource:resource-06:list',
290
+ kind: 'resource-list',
291
+ path: '/resource-06',
292
+ label: '工会小组',
293
+ navigationEligible: true,
294
+ capability: resourceDefinitions['resource-06'].capabilities.read,
295
+ resourceCode: 'resource-06',
296
+ },
297
+ {
298
+ code: 'resource:resource-07:list',
299
+ kind: 'resource-list',
300
+ path: '/resource-07',
301
+ label: '场地预约',
302
+ navigationEligible: true,
303
+ capability: resourceDefinitions['resource-07'].capabilities.read,
304
+ resourceCode: 'resource-07',
305
+ },
306
+ {
307
+ code: 'resource:resource-08:list',
308
+ kind: 'resource-list',
309
+ path: '/resource-08',
310
+ label: '场地不可用时段',
311
+ navigationEligible: true,
312
+ capability: resourceDefinitions['resource-08'].capabilities.read,
313
+ resourceCode: 'resource-08',
314
+ },
315
+ {
316
+ code: 'resource:resource-09:list',
317
+ kind: 'resource-list',
318
+ path: '/resource-09',
319
+ label: '场地档案',
320
+ navigationEligible: true,
321
+ capability: resourceDefinitions['resource-09'].capabilities.read,
322
+ resourceCode: 'resource-09',
323
+ },
324
+ {
325
+ code: 'resource:resource-10:list',
326
+ kind: 'resource-list',
327
+ path: '/resource-10',
328
+ label: '通知公告',
329
+ navigationEligible: true,
330
+ capability: resourceDefinitions['resource-10'].capabilities.read,
331
+ resourceCode: 'resource-10',
332
+ },
333
+ {
334
+ code: 'resource:resource-11:list',
335
+ kind: 'resource-list',
336
+ path: '/resource-11',
337
+ label: '活动报名',
338
+ navigationEligible: true,
339
+ capability: resourceDefinitions['resource-11'].capabilities.read,
340
+ resourceCode: 'resource-11',
341
+ },
342
+ {
343
+ code: 'resource:resource-12:list',
344
+ kind: 'resource-list',
345
+ path: '/resource-12',
346
+ label: '资料下载',
347
+ navigationEligible: true,
348
+ capability: resourceDefinitions['resource-12'].capabilities.read,
349
+ resourceCode: 'resource-12',
350
+ },
351
+ {
352
+ code: 'operation:content-operations',
353
+ kind: 'operation',
354
+ path: '/admin/operations/content',
355
+ label: '内容业务操作',
356
+ navigationEligible: true,
357
+ capability: 'app:openxiangda-application:operations:read',
358
+ routeCode: 'content-operations',
359
+ },
360
+ ]}
182
361
  contributions={adminContributions}
183
362
  resourceDefinitions={resourceDefinitions}
184
363
  />
@@ -327,16 +327,18 @@ test('updates the current user avatar through the platform-owned profile contrac
327
327
 
328
328
  test('preserves the left menu scroll position when switching resource pages', async ({ page }) => {
329
329
  await mockPlatform(page, true, runtimeBase, true);
330
+ await page.setViewportSize({ width: 1280, height: 400 });
330
331
  await page.goto('/resource-experience.e2e.html');
331
332
  const scroller = page.locator('.oxa-menu-scroll');
332
333
  await scroller.evaluate(node => {
333
- const menu = node.querySelector('.oxa-menu') as HTMLElement | null;
334
- if (menu) menu.style.minHeight = '1400px';
335
334
  node.scrollTop = 240;
336
335
  node.dispatchEvent(new Event('scroll'));
337
336
  });
338
337
  await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
339
- await page.locator('.oxa-sider').getByText('资料下载', { exact: true }).click();
338
+ await page
339
+ .locator('.oxa-sider')
340
+ .getByText('资料下载', { exact: true })
341
+ .evaluate(node => (node as HTMLElement).click());
340
342
  await expect(page).toHaveURL('/resource-12');
341
343
  await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
342
344
  });
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import {
4
+ defineWorkflowLaunchContributions,
5
+ OpenXiangdaApplication,
6
+ type AdminNavigationInput,
7
+ type AdminPagesInput,
8
+ } from 'openxiangda/react';
9
+ import { appCode } from '../../../packages/contracts/src/generated.js';
10
+ import 'openxiangda/react/styles.css';
11
+
12
+ const workflowDefinitions = [
13
+ {
14
+ code: 'purchase-approval',
15
+ title: '采购审批',
16
+ launch: { mode: 'work-center-only' },
17
+ },
18
+ ] as const;
19
+
20
+ const adminPages = [
21
+ {
22
+ code: 'workflow:work-center',
23
+ kind: 'workflow-work-center',
24
+ path: '/admin/work-center',
25
+ label: '我的审批',
26
+ navigationEligible: true,
27
+ },
28
+ {
29
+ code: 'workflow:task',
30
+ kind: 'workflow-task',
31
+ path: '/admin/tasks/:taskId',
32
+ label: '审批任务',
33
+ navigationEligible: false,
34
+ },
35
+ {
36
+ code: 'workflow:instance',
37
+ kind: 'workflow-instance',
38
+ path: '/admin/workflows/:instanceId',
39
+ label: '流程详情',
40
+ navigationEligible: false,
41
+ },
42
+ ] as const satisfies AdminPagesInput;
43
+
44
+ const adminNavigation = [
45
+ {
46
+ code: 'workflow-center',
47
+ label: '流程中心',
48
+ icon: 'workflow',
49
+ order: 0,
50
+ items: [{ pageCode: 'workflow:work-center', label: '我的审批', order: 0 }],
51
+ },
52
+ ] as const satisfies AdminNavigationInput;
53
+
54
+ const initialPath =
55
+ new URLSearchParams(window.location.search).get('initial') ||
56
+ '/admin/work-center';
57
+ if (location.pathname.endsWith('/workflow-experience.e2e.html')) {
58
+ history.replaceState({}, '', initialPath);
59
+ }
60
+
61
+ ReactDOM.createRoot(document.getElementById('root')!).render(
62
+ <React.StrictMode>
63
+ <OpenXiangdaApplication
64
+ appCode={appCode}
65
+ appName="流程体验验收"
66
+ adminNavigation={adminNavigation}
67
+ adminPages={adminPages}
68
+ resourceDefinitions={{}}
69
+ workflows={defineWorkflowLaunchContributions(workflowDefinitions, {})}
70
+ />
71
+ </React.StrictMode>
72
+ );
@@ -1,7 +1,6 @@
1
1
  import { expect, test, type Page } from '@playwright/test';
2
2
  import { appCode } from '../../../packages/contracts/src/generated.js';
3
3
 
4
- const runtimeBase = `/view/${appCode}`;
5
4
  const taskId = '11111111-1111-4111-8111-111111111111';
6
5
  const instanceId = '22222222-2222-4222-8222-222222222222';
7
6
  function envelope(data: unknown, code = 200) {
@@ -187,17 +186,6 @@ function timeline(completed = false) {
187
186
 
188
187
  async function mockWorkflow(page: Page, options: { forbidden?: boolean } = {}) {
189
188
  let completed = false;
190
- await page.route(`**${runtimeBase}/**`, async (route) => {
191
- if (route.request().resourceType() !== 'document') return route.continue();
192
- const response = await route.fetch();
193
- const body = (await response.text()).replace(
194
- '<head>',
195
- `<head><meta name="openxiangda-runtime-base" content="${runtimeBase}/" />` +
196
- `<meta name="openxiangda-app-code" content="${appCode}" />` +
197
- '<meta name="openxiangda-environment" content="preproduction" />',
198
- );
199
- await route.fulfill({ response, body });
200
- });
201
189
  await page.route('**/service/**', async (route) => {
202
190
  const url = new URL(route.request().url());
203
191
  const path = url.pathname;
@@ -306,7 +294,9 @@ test('renders a paged desktop work center from the current-user role union', asy
306
294
  page,
307
295
  }) => {
308
296
  await mockWorkflow(page);
309
- await page.goto(`${runtimeBase}/admin/work-center`);
297
+ await page.goto(
298
+ '/workflow-experience.e2e.html?initial=/admin/work-center'
299
+ );
310
300
  await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
311
301
  await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
312
302
  await expect(page.getByText('PO-2026-0825')).toBeVisible();
@@ -317,7 +307,9 @@ test('renders the independent mobile task and executes only a Surface operation'
317
307
  }) => {
318
308
  await mockWorkflow(page);
319
309
  await page.setViewportSize({ width: 390, height: 844 });
320
- await page.goto(`${runtimeBase}/m/tasks/${taskId}`);
310
+ await page.goto(
311
+ `/workflow-experience.e2e.html?initial=/m/tasks/${taskId}`
312
+ );
321
313
  await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
322
314
  await expect(page.getByText('¥28,600')).toBeVisible();
323
315
  await expect(page.getByText('财务负责人').first()).toBeVisible();
@@ -334,7 +326,7 @@ test('shows an explicit authorization error instead of an empty work center', as
334
326
  }) => {
335
327
  test.setTimeout(45_000);
336
328
  await mockWorkflow(page, { forbidden: true });
337
- await page.goto(`${runtimeBase}/admin/work-center`, {
329
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center', {
338
330
  waitUntil: 'domcontentloaded',
339
331
  });
340
332
  await expect(page.getByRole('alert')).toContainText('HTTP_403: forbidden', {
@@ -14,7 +14,7 @@
14
14
  "@ant-design/icons": "6.2.3",
15
15
  "@refinedev/core": "5.0.12",
16
16
  "antd": "6.4.3",
17
- "openxiangda": "2.0.0-alpha.27",
17
+ "openxiangda": "2.0.0-alpha.31",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import ReactDOM from 'react-dom/client';
3
3
  import {
4
4
  defineAdminContributions,
5
+ defineWorkflowLaunchContributions,
5
6
  OpenXiangdaApplication,
6
7
  } from 'openxiangda/react';
7
8
  import {
@@ -9,24 +10,32 @@ import {
9
10
  appName,
10
11
  appPerspectives,
11
12
  appRoutes,
13
+ adminNavigation,
14
+ adminPages,
12
15
  resourceDefinitions,
13
- workflowCodes,
16
+ workflowDefinitions,
14
17
  } from '../../../packages/contracts/src/generated.js';
15
18
  import 'openxiangda/react/styles.css';
16
19
 
17
20
  const adminContributions = defineAdminContributions(appRoutes, {
18
21
  pages: {},
19
22
  });
23
+ const workflowContributions = defineWorkflowLaunchContributions(
24
+ workflowDefinitions,
25
+ {},
26
+ );
20
27
 
21
28
  ReactDOM.createRoot(document.getElementById('root')!).render(
22
29
  <React.StrictMode>
23
30
  <OpenXiangdaApplication
24
31
  appCode={appCode}
25
32
  appName={appName}
33
+ adminNavigation={adminNavigation}
34
+ adminPages={adminPages}
26
35
  contributions={adminContributions}
27
36
  perspectives={appPerspectives}
28
37
  resourceDefinitions={resourceDefinitions}
29
- workflows={workflowCodes}
38
+ workflows={workflowContributions}
30
39
  />
31
40
  </React.StrictMode>,
32
41
  );
@@ -96,6 +96,28 @@ test('published E2E harnesses do not write evidence into the platform source rep
96
96
  assert.doesNotMatch(workflowSource, /docs\/architecture|evidenceDirectory/);
97
97
  });
98
98
 
99
+ test('E2E experience fixtures explicitly own every tested admin route', () => {
100
+ const resourceFixture = readFileSync(
101
+ new URL('../e2e/resource-experience-fixture.tsx', import.meta.url),
102
+ 'utf8'
103
+ );
104
+ for (let index = 1; index <= 12; index += 1) {
105
+ const code = `resource-${String(index).padStart(2, '0')}`;
106
+ const pageCode = `resource:${code}:list`;
107
+ assert.match(resourceFixture, new RegExp(`pageCode: '${pageCode}'`));
108
+ assert.match(resourceFixture, new RegExp(`code: '${pageCode}'`));
109
+ }
110
+
111
+ const workflowFixture = readFileSync(
112
+ new URL('../e2e/workflow-experience-fixture.tsx', import.meta.url),
113
+ 'utf8'
114
+ );
115
+ assert.match(workflowFixture, /code: 'workflow:work-center'/);
116
+ assert.match(workflowFixture, /code: 'workflow:task'/);
117
+ assert.match(workflowFixture, /code: 'workflow:instance'/);
118
+ assert.match(workflowFixture, /pageCode: 'workflow:work-center'/);
119
+ });
120
+
99
121
  test('browser acceptance serializes the prewarmed Vite HTML entrypoints', () => {
100
122
  const viteConfig = readFileSync(new URL('../vite.config.ts', import.meta.url), 'utf8');
101
123
  const playwrightConfig = readFileSync(
@@ -109,6 +131,7 @@ test('browser acceptance serializes the prewarmed Vite HTML entrypoints', () =>
109
131
  'data-api-live.e2e.html',
110
132
  'field-protocol.e2e.html',
111
133
  'resource-experience.e2e.html',
134
+ 'workflow-experience.e2e.html',
112
135
  ]);
113
136
  for (const entry of htmlEntries) {
114
137
  assert.ok(viteConfig.includes(`'${entry}'`), `missing optimizeDeps entry ${entry}`);
@@ -14,6 +14,7 @@ export default defineConfig({
14
14
  'data-api-live.e2e.html',
15
15
  'field-protocol.e2e.html',
16
16
  'resource-experience.e2e.html',
17
+ 'workflow-experience.e2e.html',
17
18
  ],
18
19
  },
19
20
  server: {
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="openxiangda-runtime-base" content="/" />
7
+ <meta name="openxiangda-app-code" content="openxiangda-application" />
8
+ <meta name="openxiangda-environment" content="preproduction" />
9
+ <title>Workflow experience acceptance</title>
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/e2e/workflow-experience-fixture.tsx"></script>
14
+ </body>
15
+ </html>
@@ -1,10 +1,14 @@
1
- import { defineOpenXiangdaApp } from 'openxiangda/config';
1
+ import { defineAdminNavigation, defineOpenXiangdaApp } from 'openxiangda/config';
2
2
 
3
3
  /** Clean application scaffold: declare resources and fields, then generate CRUD. */
4
4
  export default defineOpenXiangdaApp({
5
5
  schemaVersion: 3,
6
6
  app: { code: 'openxiangda-application', name: 'OpenXiangda 应用' },
7
- frontend: { root: 'apps/web', routes: [] },
7
+ frontend: {
8
+ root: 'apps/web',
9
+ routes: [],
10
+ admin: { navigation: defineAdminNavigation([]) },
11
+ },
8
12
  backend: {
9
13
  root: 'apps/server',
10
14
  runtime: 'node',
@@ -22,7 +22,7 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda": "2.0.0-alpha.27",
25
+ "openxiangda": "2.0.0-alpha.31",
26
26
  "typescript": "5.9.3"
27
27
  }
28
28
  }
@@ -17,6 +17,8 @@ export const capabilities = [
17
17
  ] as const;
18
18
  export const appOperations = {} as const;
19
19
  export const appRoutes = {} as const;
20
+ export const adminPages = [] as const;
21
+ export const adminNavigation = [] as const;
20
22
  export const eventTypes = [] as const;
21
23
  export const eventSubscriptionCodes = [] as const;
22
24
  export const eventHandlerManifest = {
@@ -27,6 +29,7 @@ export const eventHandlerManifest = {
27
29
  export const eventHandlers = {} as const;
28
30
  export const eventSchemas = [] as const;
29
31
  export const workflowCodes = [] as const;
32
+ export const workflowDefinitions = [] as const;
30
33
  export const notificationTemplateCodes = {
31
34
  applicationInformational: "application.informational.standard",
32
35
  } as const;
@@ -35,6 +38,8 @@ export type ResourceCode = (typeof resourceCodes)[number];
35
38
  export type Capability = (typeof capabilities)[number];
36
39
  export type AppOperation = (typeof appOperations)[keyof typeof appOperations];
37
40
  export type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
41
+ export type AdminPage = (typeof adminPages)[number];
42
+ export type AdminNavigationGroup = (typeof adminNavigation)[number];
38
43
  export type AppPerspective = (typeof appPerspectives)[number];
39
44
  export type EventType = (typeof eventTypes)[number];
40
45
  export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];