openxiangda-cli 2.0.0-alpha.125 → 2.0.0-alpha.127
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 +6 -1
- package/package.json +5 -5
- package/template/AGENTS.md +2 -1
- package/template/README.md +7 -0
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -21
- package/template/apps/server/test/smoke.test.ts +2 -1
- package/template/apps/web/e2e/field-protocol-fixture.tsx +3 -0
- package/template/apps/web/e2e/resource-experience-fixture.tsx +17 -1
- package/template/apps/web/e2e/resources.spec.ts +32 -1
- package/template/apps/web/e2e/theme-contract-fixture.css +15 -0
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +37 -13
- package/template/apps/web/e2e/workflow.spec.ts +123 -34
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/main.tsx +1 -7
- package/template/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# openxiangda-cli
|
|
2
2
|
|
|
3
|
-
OpenXiangda 2.0 installs one developer CLI
|
|
3
|
+
OpenXiangda 2.0 installs one developer CLI for application creation, connected development, checks, deployment status and logs, explicit retry/cancel, environment start/stop, rollback, login, AppSpec and Skill installation.
|
|
4
|
+
|
|
5
|
+
Deployment recovery is server-authoritative. `status` exposes the root and
|
|
6
|
+
latest failures, candidate identity/state, recovery decision and append-only
|
|
7
|
+
attempt ledger; `logs` preserves those fields. The CLI follows only the
|
|
8
|
+
published recovery command and never advertises cancel after activation.
|
|
4
9
|
|
|
5
10
|
The CLI disables user, development and JIT plugin loading. A clean build removes orphaned command output before TypeScript compilation. `--json` emits one stable result envelope and never mixes progress logs into machine output.
|
|
6
11
|
|
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.127",
|
|
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.63",
|
|
25
|
+
"openxiangda-devkit-core": "2.0.0-alpha.81",
|
|
26
|
+
"openxiangda-mcp": "2.0.0-alpha.81",
|
|
27
|
+
"openxiangda-skill-kit": "2.0.0-alpha.102"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "4.23.12",
|
package/template/AGENTS.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- Use the current logged-in user and the union of that user's application roles. An optional declared Perspective projects only read visibility (pages, rows and fields); create/update/delete, workflows and custom actions continue to authorize against the complete union. Standard Data API calls inherit `X-OpenXiangda-Perspective` automatically. Use `@CurrentPerspective()` only when custom Nest code reads outside the standard Data API, and apply an equivalent read projection explicitly. Business code must not persist a platform Token or authorization result and must not implement a second identity path.
|
|
15
15
|
- Fields inherit the resource read/create/update capabilities. Use field `access` only to tighten them; arrays are all-of and `false` is explicit deny. There is no `write` fallback.
|
|
16
16
|
- Field `type` is semantic, never a hand-authored database type. The supported catalog is exactly `text.short`, `text.long`, `text.rich`, `number.integer`, `number.decimal`, `boolean`, `date`, `time`, `datetime`, `date-range`, `datetime-range`, `option.single`, `option.multiple`, `cascade.single`, `cascade.multiple`, `user.single`, `user.multiple`, `department.single`, `department.multiple`, `resource-ref.single`, `resource-ref.multiple`, `file`, `image`, `signature`, `address`, `location`, `json`, `serial-number`, `uuid` and `subtable`. The compiler alone derives PostgreSQL columns, constraints and indexes.
|
|
17
|
+
- Every `date-range` and `datetime-range` field explicitly declares `rangeBoundary: 'closed' | 'half-open'`. Values remain `{ start, end }`; callers never send or override the boundary. Closed ranges allow `start <= end` and use PostgreSQL `[]`; half-open ranges require `start < end` and use `[)`.
|
|
17
18
|
- Declare every custom operation capability in `authz.capabilities` with `kind: 'backend'`, then reference that same code from the operation and its allowed roles. Generated resource CRUD capabilities do not go in this catalog.
|
|
18
19
|
- Build App Operation request/response schemas with `resourceRecordSchema`, `schemaRef`, `composeJsonSchema` and `composeAppOperationSchemas` from `openxiangda/config`. They project the same Resource declaration and canonical field value protocols; never import a physical toolchain package or repeat user, department, resource-reference or managed-file schemas by hand.
|
|
19
20
|
- Visitor duplicate protection uses `createVisitorReservation({ duplicateMatch: { fieldCode: submittedValue }, ... })`. `duplicateMatch` is a non-empty value map, never a field-name array, and no mutable pre-read is allowed.
|
|
@@ -21,6 +22,6 @@
|
|
|
21
22
|
- `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`.
|
|
22
23
|
- Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
|
|
23
24
|
- 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.
|
|
24
|
-
- Standard Workflow and Notification Hub are optional 2.0 modules. Enable them only through
|
|
25
|
+
- 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` use the compiler-owned process operation and Durable Business Process Command; 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 `adminApplicationTodoCenterPage()`; never query Notification Hub management APIs from a user page.
|
|
25
26
|
- 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.
|
|
26
27
|
- 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
|
@@ -26,6 +26,13 @@ BrowserRouter、RuntimeBoundary、Refine、generated routes 和后台 Shell
|
|
|
26
26
|
在显式后台导航组中加入 `adminApplicationTodoCenterPage()`;平台生成
|
|
27
27
|
`/admin/todos` 与 `/m/todos`,只读取当前用户的消息/待办投影并统一解析目标页面。
|
|
28
28
|
|
|
29
|
+
自定义后台页使用 `OpenXiangdaAdminPage` 作为页面根;Ant Design 组件自动继承平台
|
|
30
|
+
`ConfigProvider`,图表等 TypeScript 渲染值读取 `useOpenXiangdaTheme()`,自定义 CSS
|
|
31
|
+
只使用公开的 `--oxa-color-*`、`--oxa-shadow-surface` 和
|
|
32
|
+
`--oxa-radius-surface` 语义变量。不要为后台页新建主题 Provider,也不要把白色、黑色、
|
|
33
|
+
浅灰色写死为页面、卡片、边框或文字颜色。品牌色只能用于品牌和有明确语义的数据分类,
|
|
34
|
+
不能替代结构色。
|
|
35
|
+
|
|
29
36
|
登录声明位于 `frontend.authentication`,只允许现有平台用户并拒绝注册。桌面 `/login`
|
|
30
37
|
与移动 `/m/login` 是独立生成的 `authenticationSurfaces`,不计入受保护的
|
|
31
38
|
`appRoutes`。用 `defineApplicationContributions({ routes: appRoutes,
|
|
@@ -11,27 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
@Module({
|
|
13
13
|
imports: [
|
|
14
|
-
OpenXiangdaModule.
|
|
15
|
-
appCode: process.env.OPENXIANGDA_APP_CODE || 'openxiangda-application',
|
|
16
|
-
platformBaseUrl:
|
|
17
|
-
process.env.OPENXIANGDA_PLATFORM_BASE_URL ||
|
|
18
|
-
'http://127.0.0.1:7001/service',
|
|
19
|
-
environmentKey:
|
|
20
|
-
process.env.OPENXIANGDA_ENVIRONMENT_KEY || 'preproduction',
|
|
21
|
-
environmentId:
|
|
22
|
-
process.env.OPENXIANGDA_ENVIRONMENT_ID || 'connected-development',
|
|
23
|
-
appVersionId:
|
|
24
|
-
process.env.OPENXIANGDA_APP_VERSION_ID || 'connected-development',
|
|
25
|
-
deploymentRunId:
|
|
26
|
-
process.env.OPENXIANGDA_DEPLOYMENT_RUN_ID || 'connected-development',
|
|
27
|
-
environmentHeadRevision: Number(
|
|
28
|
-
process.env.OPENXIANGDA_ENVIRONMENT_HEAD_REVISION || 1
|
|
29
|
-
),
|
|
30
|
-
backendRevisionId:
|
|
31
|
-
process.env.OPENXIANGDA_BACKEND_REVISION_ID || 'connected-development',
|
|
32
|
-
version: process.env.OPENXIANGDA_APP_VERSION || '0.1.0-dev',
|
|
33
|
-
connectedDevelopment:
|
|
34
|
-
process.env.OPENXIANGDA_CONNECTED_DEV === 'true',
|
|
14
|
+
OpenXiangdaModule.forApplication({
|
|
35
15
|
eventHandlerManifest,
|
|
36
16
|
eventSchemas,
|
|
37
17
|
eventSigningSecrets: eventSigningSecretsFromEnvironment(
|
|
@@ -5,7 +5,8 @@ import test from 'node:test';
|
|
|
5
5
|
test('the clean server template only owns platform bootstrap', () => {
|
|
6
6
|
const source = readFileSync(new URL('../src/app.module.ts', import.meta.url), 'utf8');
|
|
7
7
|
const main = readFileSync(new URL('../src/main.ts', import.meta.url), 'utf8');
|
|
8
|
-
assert.match(source, /OpenXiangdaModule\.
|
|
8
|
+
assert.match(source, /OpenXiangdaModule\.forApplication/);
|
|
9
|
+
assert.doesNotMatch(source, /OPENXIANGDA_(?:APP_CODE|ENVIRONMENT_ID|OAUTH_CLIENT_ID)/);
|
|
9
10
|
assert.match(source, /eventHandlerManifest/);
|
|
10
11
|
assert.match(source, /eventSigningSecretsFromEnvironment/);
|
|
11
12
|
assert.match(main, /bootstrapOpenXiangdaApplication\(AppModule\)/);
|
|
@@ -3,10 +3,13 @@ import ReactDOM from 'react-dom/client';
|
|
|
3
3
|
import { Button, Typography } from 'antd';
|
|
4
4
|
import {
|
|
5
5
|
defineApplicationContributions,
|
|
6
|
+
OpenXiangdaAdminPage,
|
|
6
7
|
OpenXiangdaApplication,
|
|
8
|
+
useOpenXiangdaTheme,
|
|
7
9
|
} from 'openxiangda/react';
|
|
8
10
|
import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
|
|
9
11
|
import 'openxiangda/react/styles.css';
|
|
12
|
+
import './theme-contract-fixture.css';
|
|
10
13
|
|
|
11
14
|
const field = (
|
|
12
15
|
label: string,
|
|
@@ -177,7 +180,20 @@ const resourceDefinitions = Object.fromEntries(
|
|
|
177
180
|
);
|
|
178
181
|
|
|
179
182
|
function ContentOperationsPage() {
|
|
180
|
-
|
|
183
|
+
const { effectiveAppearance, tokens } = useOpenXiangdaTheme();
|
|
184
|
+
return (
|
|
185
|
+
<OpenXiangdaAdminPage
|
|
186
|
+
className="theme-contract-page"
|
|
187
|
+
data-primary={tokens.primary}
|
|
188
|
+
data-theme={effectiveAppearance}
|
|
189
|
+
data-testid="custom-admin-page"
|
|
190
|
+
>
|
|
191
|
+
<Typography.Title level={2}>内容业务操作</Typography.Title>
|
|
192
|
+
<div className="theme-contract-panel">
|
|
193
|
+
自定义后台页语义主题面板
|
|
194
|
+
</div>
|
|
195
|
+
</OpenXiangdaAdminPage>
|
|
196
|
+
);
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
function ResourcePortalPage() {
|
|
@@ -71,7 +71,12 @@ function runtimeAuthorization(authorized: boolean, superAdmin = false) {
|
|
|
71
71
|
type: 'user_union',
|
|
72
72
|
userId: 'acceptance-user',
|
|
73
73
|
roleCodes: ['college_admin', 'instrument_admin'],
|
|
74
|
-
capabilityCodes: authorized
|
|
74
|
+
capabilityCodes: authorized
|
|
75
|
+
? [
|
|
76
|
+
...allCapabilities,
|
|
77
|
+
'app:openxiangda-application:operations:read',
|
|
78
|
+
]
|
|
79
|
+
: [],
|
|
75
80
|
isAppSuperAdmin: superAdmin,
|
|
76
81
|
identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
|
|
77
82
|
},
|
|
@@ -281,6 +286,32 @@ test('renders the compiled desktop resource contract and shared workbench', asyn
|
|
|
281
286
|
await expect.poll(() => page.evaluate(() => localStorage.getItem('openxiangda.admin.appearance'))).toBe('dark');
|
|
282
287
|
});
|
|
283
288
|
|
|
289
|
+
test('custom admin pages inherit tokens and follow live system appearance', async ({ page }) => {
|
|
290
|
+
await page.emulateMedia({ colorScheme: 'dark' });
|
|
291
|
+
await mockPlatform(page);
|
|
292
|
+
await page.goto('/resource-experience.e2e.html');
|
|
293
|
+
await page
|
|
294
|
+
.locator('.oxa-sider')
|
|
295
|
+
.getByText('内容业务操作', { exact: true })
|
|
296
|
+
.click();
|
|
297
|
+
|
|
298
|
+
const customPage = page.getByTestId('custom-admin-page');
|
|
299
|
+
const customPanel = customPage.locator('.theme-contract-panel');
|
|
300
|
+
await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
|
|
301
|
+
await expect(customPage).toHaveAttribute('data-theme', 'dark');
|
|
302
|
+
await expect(customPage).toHaveAttribute('data-primary', '#447bdc');
|
|
303
|
+
await expect(customPage).toHaveCSS('background-color', 'rgb(11, 21, 36)');
|
|
304
|
+
await expect(customPanel).toHaveCSS('background-color', 'rgb(17, 29, 46)');
|
|
305
|
+
await expect(customPanel).toHaveCSS('color', 'rgb(215, 224, 236)');
|
|
306
|
+
await expect(customPanel).toHaveCSS('border-color', 'rgb(38, 55, 79)');
|
|
307
|
+
|
|
308
|
+
await page.emulateMedia({ colorScheme: 'light' });
|
|
309
|
+
await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'light');
|
|
310
|
+
await expect(customPage).toHaveAttribute('data-theme', 'light');
|
|
311
|
+
await expect(customPage).toHaveCSS('background-color', 'rgb(245, 245, 245)');
|
|
312
|
+
await expect(customPanel).toHaveCSS('background-color', 'rgb(255, 255, 255)');
|
|
313
|
+
});
|
|
314
|
+
|
|
284
315
|
test('opens the stable admin entry under the preproduction gateway mount', async ({ page }) => {
|
|
285
316
|
test.skip(!codes.length, 'No resource route exists in an empty application.');
|
|
286
317
|
const devRuntimeBase = `/dev/${appCode}`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.theme-contract-page {
|
|
2
|
+
display: grid;
|
|
3
|
+
align-content: start;
|
|
4
|
+
gap: 16px;
|
|
5
|
+
padding: 24px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.theme-contract-panel {
|
|
9
|
+
border: 1px solid var(--oxa-color-border);
|
|
10
|
+
border-radius: var(--oxa-radius-surface);
|
|
11
|
+
background: var(--oxa-color-surface);
|
|
12
|
+
padding: 24px;
|
|
13
|
+
color: var(--oxa-color-text);
|
|
14
|
+
box-shadow: var(--oxa-shadow-surface);
|
|
15
|
+
}
|
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
3
|
import {
|
|
4
4
|
defineApplicationContributions,
|
|
5
|
-
defineWorkflowLaunchContributions,
|
|
6
5
|
OpenXiangdaApplication,
|
|
7
6
|
type AdminNavigationInput,
|
|
8
7
|
type AdminPagesInput,
|
|
@@ -16,9 +15,44 @@ const workflowDefinitions = [
|
|
|
16
15
|
code: 'purchase-approval',
|
|
17
16
|
title: '采购审批',
|
|
18
17
|
launch: { mode: 'standalone' },
|
|
18
|
+
subject: {
|
|
19
|
+
resourceCode: 'purchase-orders',
|
|
20
|
+
factProjection: { amount: 'amount' },
|
|
21
|
+
},
|
|
22
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
19
23
|
},
|
|
20
24
|
] as const;
|
|
21
25
|
|
|
26
|
+
const resourceDefinitions = {
|
|
27
|
+
'purchase-orders': {
|
|
28
|
+
code: 'purchase-orders',
|
|
29
|
+
name: '采购申请',
|
|
30
|
+
capabilities: {
|
|
31
|
+
read: `app:${appCode}:data:purchase-orders:read`,
|
|
32
|
+
create: `app:${appCode}:data:purchase-orders:create`,
|
|
33
|
+
update: `app:${appCode}:data:purchase-orders:update`,
|
|
34
|
+
delete: `app:${appCode}:data:purchase-orders:delete`,
|
|
35
|
+
},
|
|
36
|
+
surface: {
|
|
37
|
+
mutationOwner: 'native',
|
|
38
|
+
generated: { list: true, detail: true, create: true, update: true, delete: true },
|
|
39
|
+
fields: {
|
|
40
|
+
amount: {
|
|
41
|
+
label: '申请金额',
|
|
42
|
+
type: 'number.decimal',
|
|
43
|
+
widget: 'money',
|
|
44
|
+
requiredHint: true,
|
|
45
|
+
readCapabilities: [`app:${appCode}:data:purchase-orders:read`],
|
|
46
|
+
createCapabilities: [`app:${appCode}:data:purchase-orders:create`],
|
|
47
|
+
updateCapabilities: [`app:${appCode}:data:purchase-orders:update`],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
form: { layout: 'flat', fieldOrder: ['amount'] },
|
|
51
|
+
mobile: { enabled: true },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
22
56
|
const detailRoutes = {
|
|
23
57
|
desktop: {
|
|
24
58
|
code: 'purchase-detail',
|
|
@@ -127,18 +161,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
127
161
|
adminNavigation={adminNavigation}
|
|
128
162
|
adminPages={adminPages}
|
|
129
163
|
contributions={applicationContributions}
|
|
130
|
-
resourceDefinitions={
|
|
131
|
-
workflows={
|
|
132
|
-
'purchase-approval': async () => ({
|
|
133
|
-
businessKey: 'PO-2026-0825',
|
|
134
|
-
dataRef: {
|
|
135
|
-
resourceCode: 'purchase-orders',
|
|
136
|
-
recordId: 'purchase-1',
|
|
137
|
-
dataRevision: '8',
|
|
138
|
-
},
|
|
139
|
-
facts: { amount: 28600, applicant: '张三' },
|
|
140
|
-
}),
|
|
141
|
-
})}
|
|
164
|
+
resourceDefinitions={resourceDefinitions}
|
|
165
|
+
workflows={workflowDefinitions}
|
|
142
166
|
/>
|
|
143
167
|
</React.StrictMode>
|
|
144
168
|
);
|
|
@@ -3,6 +3,7 @@ import { appCode } from '../../../packages/contracts/src/generated.js';
|
|
|
3
3
|
|
|
4
4
|
const taskId = '11111111-1111-4111-8111-111111111111';
|
|
5
5
|
const instanceId = '22222222-2222-4222-8222-222222222222';
|
|
6
|
+
const commandId = '55555555-5555-4555-8555-555555555555';
|
|
6
7
|
function envelope(data: unknown, code = 200) {
|
|
7
8
|
return { code, message: code === 200 ? 'success' : 'forbidden', data };
|
|
8
9
|
}
|
|
@@ -53,6 +54,8 @@ function surface(completed = false, customDetail = false) {
|
|
|
53
54
|
protocolVersion: 'workflow_surface_v2',
|
|
54
55
|
surfaceRevision: completed ? 'revision-2' : 'revision-1',
|
|
55
56
|
engineVersion: '2.0',
|
|
57
|
+
commandToken: 'A'.repeat(43),
|
|
58
|
+
commandTokenExpiresAt: '2099-08-29T09:05:00.000Z',
|
|
56
59
|
instanceSequence: completed ? 4 : 3,
|
|
57
60
|
detailNavigation: detailNavigation(customDetail),
|
|
58
61
|
navigationTarget: {
|
|
@@ -212,6 +215,11 @@ function launchSurface() {
|
|
|
212
215
|
workflowCode: 'purchase-approval',
|
|
213
216
|
title: '采购审批',
|
|
214
217
|
launchMode: 'standalone',
|
|
218
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
219
|
+
subject: {
|
|
220
|
+
resourceCode: 'purchase-orders',
|
|
221
|
+
factProjection: { amount: 'amount' },
|
|
222
|
+
},
|
|
215
223
|
inputSchema: { type: 'object', properties: {} },
|
|
216
224
|
head: {
|
|
217
225
|
workflowRevision: 4,
|
|
@@ -224,18 +232,85 @@ function launchSurface() {
|
|
|
224
232
|
desktop: '/admin/workflows/purchase-approval/new',
|
|
225
233
|
mobile: '/m/workflows/purchase-approval/start',
|
|
226
234
|
},
|
|
227
|
-
|
|
228
|
-
method: 'POST',
|
|
229
|
-
href: '/workflow/definitions/purchase-approval/prepare-start',
|
|
230
|
-
},
|
|
231
|
-
start: {
|
|
235
|
+
commit: {
|
|
232
236
|
method: 'POST',
|
|
233
|
-
href:
|
|
237
|
+
href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
|
|
234
238
|
idempotencyRequired: true,
|
|
235
239
|
},
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
function processCommand(status: 'accepted' | 'started' = 'started') {
|
|
244
|
+
return {
|
|
245
|
+
schemaVersion: 'openxiangda.business-process.command/v2',
|
|
246
|
+
id: commandId,
|
|
247
|
+
appCode,
|
|
248
|
+
environmentKey: 'preproduction',
|
|
249
|
+
operationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
250
|
+
idempotencyKey: 'process:purchase-approval:test',
|
|
251
|
+
workflowCode: 'purchase-approval',
|
|
252
|
+
status,
|
|
253
|
+
revision: status === 'started' ? 4 : 1,
|
|
254
|
+
subject: {
|
|
255
|
+
resourceCode: 'purchase-orders',
|
|
256
|
+
id: '66666666-6666-4666-8666-666666666666',
|
|
257
|
+
dataRevision: 1,
|
|
258
|
+
factDigest: 'a'.repeat(64),
|
|
259
|
+
},
|
|
260
|
+
definitionVersion: 1,
|
|
261
|
+
bindingVersion: 1,
|
|
262
|
+
requirements: [],
|
|
263
|
+
answers: {},
|
|
264
|
+
preview: {},
|
|
265
|
+
workflowInstanceId: status === 'started' ? instanceId : null,
|
|
266
|
+
attemptCount: 1,
|
|
267
|
+
lastError: null,
|
|
268
|
+
replayed: false,
|
|
269
|
+
createdAt: '2026-08-29T01:00:00.000Z',
|
|
270
|
+
updatedAt: '2026-08-29T01:00:01.000Z',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function processCommandSurface() {
|
|
275
|
+
const command = processCommand('started');
|
|
276
|
+
const base = `/openxiangda-api/v2/applications/${appCode}/business-process/commands/${commandId}`;
|
|
277
|
+
return {
|
|
278
|
+
schemaVersion: 'openxiangda.process-command-surface/v2',
|
|
279
|
+
surfaceRevision: 'b'.repeat(64),
|
|
280
|
+
command,
|
|
281
|
+
subject: {
|
|
282
|
+
resourceCode: 'purchase-orders',
|
|
283
|
+
recordId: command.subject.id,
|
|
284
|
+
dataRevision: 1,
|
|
285
|
+
form: {
|
|
286
|
+
kind: 'native-resource-form',
|
|
287
|
+
resourceCode: 'purchase-orders',
|
|
288
|
+
recordId: command.subject.id,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
requirements: [],
|
|
292
|
+
resume: {
|
|
293
|
+
status: { method: 'GET', href: base },
|
|
294
|
+
surface: { method: 'GET', href: `${base}/surface` },
|
|
295
|
+
answer: null,
|
|
296
|
+
retry: null,
|
|
297
|
+
navigationTarget: {
|
|
298
|
+
kind: 'PLATFORM_ROUTE',
|
|
299
|
+
routeCode: 'resource.record',
|
|
300
|
+
appCode,
|
|
301
|
+
pathParams: {
|
|
302
|
+
resourceCode: 'purchase-orders',
|
|
303
|
+
recordId: command.subject.id,
|
|
304
|
+
},
|
|
305
|
+
query: { processCommandId: commandId },
|
|
306
|
+
access: 'AUTHENTICATED',
|
|
307
|
+
},
|
|
308
|
+
desktop: `/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
|
|
309
|
+
mobile: `/m/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
239
314
|
function todoPage() {
|
|
240
315
|
return {
|
|
241
316
|
schemaVersion: 'openxiangda.application-todo-center/v2',
|
|
@@ -283,6 +358,12 @@ async function mockWorkflow(
|
|
|
283
358
|
await page.route('**/service/**', async route => {
|
|
284
359
|
const url = new URL(route.request().url());
|
|
285
360
|
const path = url.pathname;
|
|
361
|
+
if (path.endsWith('/auth/surface')) {
|
|
362
|
+
return route.fulfill({
|
|
363
|
+
contentType: 'application/json',
|
|
364
|
+
body: JSON.stringify(envelope({ csrfToken: 'workflow-csrf-1' })),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
286
367
|
if (path.endsWith('/native/authz/current')) {
|
|
287
368
|
return route.fulfill({
|
|
288
369
|
contentType: 'application/json',
|
|
@@ -319,7 +400,7 @@ async function mockWorkflow(
|
|
|
319
400
|
userId: 'acceptance-user',
|
|
320
401
|
roleCodes: ['department_manager'],
|
|
321
402
|
capabilityCodes: [],
|
|
322
|
-
isAppSuperAdmin:
|
|
403
|
+
isAppSuperAdmin: true,
|
|
323
404
|
identityScope:
|
|
324
405
|
'user-union:preproduction-id:acceptance-user:authz-1',
|
|
325
406
|
},
|
|
@@ -369,40 +450,36 @@ async function mockWorkflow(
|
|
|
369
450
|
});
|
|
370
451
|
}
|
|
371
452
|
if (
|
|
372
|
-
path.endsWith(
|
|
373
|
-
|
|
374
|
-
)
|
|
453
|
+
path.endsWith('/business-process/standard-commands') &&
|
|
454
|
+
route.request().method() === 'POST'
|
|
375
455
|
) {
|
|
456
|
+
const body = route.request().postDataJSON();
|
|
457
|
+
expect(body).toMatchObject({
|
|
458
|
+
schemaVersion: 'openxiangda.standard-process.commit/v2',
|
|
459
|
+
environmentKey: 'preproduction',
|
|
460
|
+
workflowCode: 'purchase-approval',
|
|
461
|
+
mutation: { kind: 'create', data: { amount: '28600' } },
|
|
462
|
+
});
|
|
463
|
+
for (const forbidden of [
|
|
464
|
+
'facts',
|
|
465
|
+
'dataRef',
|
|
466
|
+
'preparationToken',
|
|
467
|
+
'processOperationCode',
|
|
468
|
+
]) {
|
|
469
|
+
expect(body).not.toHaveProperty(forbidden);
|
|
470
|
+
}
|
|
376
471
|
return route.fulfill({
|
|
377
472
|
contentType: 'application/json',
|
|
378
|
-
body: JSON.stringify(
|
|
379
|
-
envelope({
|
|
380
|
-
schemaVersion: 'openxiangda.workflow-preparation/v2',
|
|
381
|
-
preparationId: 'preparation-1',
|
|
382
|
-
status: 'ready',
|
|
383
|
-
requirements: [],
|
|
384
|
-
preparationToken: 'preparation-token-1',
|
|
385
|
-
preview: { nodes: [] },
|
|
386
|
-
expiresAt: '2026-08-28T02:00:00.000Z',
|
|
387
|
-
definitionVersion: 1,
|
|
388
|
-
bindingVersion: 1,
|
|
389
|
-
inputHash: 'input-hash-1',
|
|
390
|
-
})
|
|
391
|
-
),
|
|
473
|
+
body: JSON.stringify(envelope(processCommand('accepted'))),
|
|
392
474
|
});
|
|
393
475
|
}
|
|
394
476
|
if (
|
|
395
|
-
path.endsWith(
|
|
396
|
-
route.request().method() === '
|
|
477
|
+
path.endsWith(`/business-process/commands/${commandId}/surface`) &&
|
|
478
|
+
route.request().method() === 'GET'
|
|
397
479
|
) {
|
|
398
480
|
return route.fulfill({
|
|
399
481
|
contentType: 'application/json',
|
|
400
|
-
body: JSON.stringify(
|
|
401
|
-
envelope({
|
|
402
|
-
...surface(false, options.customDetail).instance,
|
|
403
|
-
id: instanceId,
|
|
404
|
-
})
|
|
405
|
-
),
|
|
482
|
+
body: JSON.stringify(envelope(processCommandSurface())),
|
|
406
483
|
});
|
|
407
484
|
}
|
|
408
485
|
if (path.endsWith('/workflow/work-center/items')) {
|
|
@@ -445,6 +522,19 @@ async function mockWorkflow(
|
|
|
445
522
|
});
|
|
446
523
|
}
|
|
447
524
|
if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
|
|
525
|
+
expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
|
|
526
|
+
'workflow-csrf-1'
|
|
527
|
+
);
|
|
528
|
+
expect(route.request().postDataJSON()).toMatchObject({
|
|
529
|
+
commandToken: 'A'.repeat(43),
|
|
530
|
+
input: { comment: '同意采购' },
|
|
531
|
+
});
|
|
532
|
+
expect(route.request().postDataJSON()).not.toHaveProperty(
|
|
533
|
+
'expectedTaskVersion'
|
|
534
|
+
);
|
|
535
|
+
expect(route.request().postDataJSON()).not.toHaveProperty(
|
|
536
|
+
'expectedInstanceVersion'
|
|
537
|
+
);
|
|
448
538
|
completed = true;
|
|
449
539
|
return route.fulfill({
|
|
450
540
|
contentType: 'application/json',
|
|
@@ -507,8 +597,7 @@ test('launches the same workflow from the canonical mobile Surface and follows c
|
|
|
507
597
|
);
|
|
508
598
|
await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
|
|
509
599
|
await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
|
|
510
|
-
await page.getByRole('
|
|
511
|
-
await expect(page.getByRole('button', { name: '提交审批' })).toBeVisible();
|
|
600
|
+
await page.getByRole('spinbutton', { name: '请输入申请金额' }).fill('28600');
|
|
512
601
|
await page.getByRole('button', { name: '提交审批' }).click();
|
|
513
602
|
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
514
603
|
`${instanceId} / ${taskId}`
|
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
3
|
import {
|
|
4
4
|
defineApplicationContributions,
|
|
5
|
-
defineWorkflowLaunchContributions,
|
|
6
5
|
DefaultDesktopApplicationLoginSurface,
|
|
7
6
|
DefaultMobileApplicationLoginSurface,
|
|
8
7
|
OpenXiangdaApplication,
|
|
@@ -43,11 +42,6 @@ const applicationContributions = defineApplicationContributions(
|
|
|
43
42
|
},
|
|
44
43
|
},
|
|
45
44
|
);
|
|
46
|
-
const workflowContributions = defineWorkflowLaunchContributions(
|
|
47
|
-
workflowDefinitions,
|
|
48
|
-
{},
|
|
49
|
-
);
|
|
50
|
-
|
|
51
45
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
52
46
|
<React.StrictMode>
|
|
53
47
|
<OpenXiangdaApplication
|
|
@@ -58,7 +52,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
58
52
|
contributions={applicationContributions}
|
|
59
53
|
perspectives={appPerspectives}
|
|
60
54
|
resourceDefinitions={resourceDefinitions}
|
|
61
|
-
workflows={
|
|
55
|
+
workflows={workflowDefinitions}
|
|
62
56
|
/>
|
|
63
57
|
</React.StrictMode>,
|
|
64
58
|
);
|
package/template/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by OpenXiangda 2.0 Native compiler. Do not edit by hand.
|
|
2
|
-
export const compilerContractVersion = "native-
|
|
2
|
+
export const compilerContractVersion = "native-3" as const;
|
|
3
3
|
export const appCode = "openxiangda-application" as const;
|
|
4
4
|
export const appName = "OpenXiangda 应用" as const;
|
|
5
5
|
export const appPerspectives = [] as const;
|