openxiangda-cli 2.0.0-alpha.116 → 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 +5 -2
- package/package.json +5 -5
- package/template/AGENTS.md +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/data-api-live-fixture.tsx +8 -1
- package/template/apps/web/e2e/resource-experience-fixture.tsx +181 -2
- package/template/apps/web/e2e/resources.spec.ts +5 -3
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +72 -0
- package/template/apps/web/e2e/workflow.spec.ts +7 -15
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/main.tsx +11 -2
- package/template/apps/web/test/contracts.test.ts +23 -0
- package/template/apps/web/vite.config.ts +1 -0
- package/template/apps/web/workflow-experience.e2e.html +15 -0
- package/template/openxiangda.config.ts +6 -2
- package/template/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +5 -0
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
|
|
11
|
-
|
|
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.
|
|
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.
|
|
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.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",
|
package/template/AGENTS.md
CHANGED
|
@@ -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;
|
|
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`;
|
|
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.
|
|
@@ -35,7 +35,14 @@ export const liveDefinition: GeneratedResourceDefinition = {
|
|
|
35
35
|
name: '字段协议记录',
|
|
36
36
|
capabilities,
|
|
37
37
|
surface: {
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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', {
|
|
@@ -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
|
-
|
|
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={
|
|
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}`);
|
|
@@ -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: {
|
|
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',
|
package/template/package.json
CHANGED
|
@@ -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];
|