openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.90
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 +2 -2
- package/bin/run.js +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +3 -3
- package/dist/create-workspace.js.map +1 -1
- package/dist/toolchain-capsule.d.ts.map +1 -1
- package/dist/toolchain-capsule.js +4 -8
- package/dist/toolchain-capsule.js.map +1 -1
- package/package.json +6 -7
- package/template/AGENTS.md +2 -1
- package/template/README.md +3 -3
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -1
- package/template/apps/server/src/main.ts +1 -1
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
- package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
- package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
- package/template/apps/web/e2e/resources.spec.ts +20 -181
- package/template/apps/web/field-protocol.e2e.html +3 -0
- package/template/apps/web/package.json +2 -5
- package/template/apps/web/src/main.tsx +12 -60
- package/template/apps/web/test/contracts.test.ts +52 -104
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +1 -3
- package/template/packages/contracts/src/generated.ts +1 -0
- package/template/scripts/verify-template-budget.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
- package/template/apps/web/src/FilePreviewPage.tsx +0 -324
- package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
- package/template/apps/web/src/Shell.tsx +0 -445
- package/template/apps/web/src/appearance.tsx +0 -147
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
- package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
- package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
- package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
- package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
- package/template/apps/web/src/components/resource/resource-import.ts +0 -205
- package/template/apps/web/src/data-provider.ts +0 -72
- package/template/apps/web/src/platform-client.ts +0 -708
- package/template/apps/web/src/runtime-meta.ts +0 -75
- package/template/apps/web/src/runtime.tsx +0 -227
- package/template/apps/web/src/styles.css +0 -1941
- package/template/apps/web/test/address-field.test.ts +0 -66
- package/template/apps/web/test/cascade-value.test.ts +0 -68
- package/template/apps/web/test/directory-value.test.ts +0 -55
- package/template/apps/web/test/field-bounds.test.ts +0 -17
- package/template/apps/web/test/field-form-codec.test.ts +0 -85
- package/template/apps/web/test/location-field.test.ts +0 -83
- package/template/apps/web/test/resource-import.test.ts +0 -143
- package/template/apps/web/test/resource-query.test.ts +0 -144
- package/template/apps/web/test/rich-json-field.test.ts +0 -53
- package/template/apps/web/test/signature-serial-field.test.ts +0 -40
- package/template/apps/web/test/subtable-value.test.ts +0 -144
|
@@ -23,13 +23,32 @@ test.describe('Field Kit protocol acceptance', () => {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
|
+
await page.route('**/files/*/content?*', async route => {
|
|
27
|
+
await route.fulfill({
|
|
28
|
+
body: Buffer.from(
|
|
29
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
30
|
+
'base64'
|
|
31
|
+
),
|
|
32
|
+
contentType: 'image/png',
|
|
33
|
+
status: 200,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
26
36
|
});
|
|
27
37
|
|
|
28
38
|
test('renders and operates every desktop edit/read component', async ({ page }) => {
|
|
39
|
+
const pageErrors: string[] = [];
|
|
40
|
+
const addressRequests: string[] = [];
|
|
41
|
+
page.on('pageerror', error => pageErrors.push(error.message));
|
|
42
|
+
page.on('request', request => {
|
|
43
|
+
if (request.url().includes('/china-divisions/')) {
|
|
44
|
+
addressRequests.push(request.url());
|
|
45
|
+
}
|
|
46
|
+
});
|
|
29
47
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
30
48
|
await page.goto('/field-protocol.e2e.html');
|
|
31
49
|
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
32
50
|
await expect(page.locator('[data-read-field]')).toHaveCount(fieldCount);
|
|
51
|
+
await expect(page.locator('[data-audit-field]')).toHaveCount(fieldCount);
|
|
33
52
|
await expect(page.locator('[data-field-code="静态单选下拉"] .ant-select')).toBeVisible();
|
|
34
53
|
await expect(page.locator('[data-field-code="单选按钮"] .ant-radio-group')).toBeVisible();
|
|
35
54
|
await expect(page.locator('[data-field-code="复选框"] .ant-checkbox-group')).toBeVisible();
|
|
@@ -37,6 +56,22 @@ test.describe('Field Kit protocol acceptance', () => {
|
|
|
37
56
|
await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
|
|
38
57
|
await expect(page.locator('[data-field-code="精确定位"]')).not.toContainText('手工');
|
|
39
58
|
await expect(page.locator('.ant-alert-error')).toHaveCount(0);
|
|
59
|
+
await expect.poll(() => addressRequests.length).toBeGreaterThan(0);
|
|
60
|
+
expect(addressRequests.every(url => new URL(url).pathname.startsWith('/service/china-divisions/'))).toBe(true);
|
|
61
|
+
|
|
62
|
+
await page.getByRole('button', { name: '验证表单' }).click();
|
|
63
|
+
await expect(page.locator('[data-field-code="UUID"] .ant-form-item-has-error')).toHaveCount(0);
|
|
64
|
+
|
|
65
|
+
await expect(page.locator('[data-read-field="图片"] img.oxa-file-thumbnail')).toHaveAttribute('src', /blob:/);
|
|
66
|
+
await expect(page.locator('[data-read-field="富文本"] img')).toHaveAttribute('src', /blob:/);
|
|
67
|
+
expect(await page.locator('[data-read-field="图片"] img.oxa-file-thumbnail').evaluate(
|
|
68
|
+
image => (image as HTMLImageElement).naturalWidth
|
|
69
|
+
)).toBeGreaterThan(0);
|
|
70
|
+
expect(await page.locator('[data-read-field="富文本"] img').evaluate(
|
|
71
|
+
image => (image as HTMLImageElement).naturalWidth
|
|
72
|
+
)).toBeGreaterThan(0);
|
|
73
|
+
await expect(page.locator('[aria-label="当前协议字段变更记录"]')).not.toContainText('[object Object]');
|
|
74
|
+
await expect(page.locator('[data-audit-digest="rich-text"]')).toContainText('大值摘要');
|
|
40
75
|
|
|
41
76
|
await page.locator('[data-field-code="精确定位"]').getByRole('button', { name: '浏览器定位' }).click();
|
|
42
77
|
await expect(page.locator('[data-field-code="精确定位"]')).toContainText('120.123456');
|
|
@@ -78,12 +113,63 @@ test.describe('Field Kit protocol acceptance', () => {
|
|
|
78
113
|
await saveSignature.click();
|
|
79
114
|
await expect(page.locator('[data-field-code="业务签名"]')).not.toContainText('尚未签名');
|
|
80
115
|
|
|
116
|
+
expect(pageErrors).toEqual([]);
|
|
117
|
+
|
|
81
118
|
expect(
|
|
82
119
|
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
|
83
120
|
).toBe(true);
|
|
84
121
|
await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-desktop.png' });
|
|
85
122
|
});
|
|
86
123
|
|
|
124
|
+
test('resource selectors query once when selection changes', async ({ page }) => {
|
|
125
|
+
const requestCounts = new Map<string, number>();
|
|
126
|
+
await page.route('**/source/query', async route => {
|
|
127
|
+
const match = new URL(route.request().url()).pathname.match(/\/fields\/([^/]+)\/source\/query$/);
|
|
128
|
+
const fieldCode = decodeURIComponent(match?.[1] || 'unknown');
|
|
129
|
+
requestCounts.set(fieldCode, (requestCounts.get(fieldCode) || 0) + 1);
|
|
130
|
+
const body = route.request().postDataJSON() as { operation?: 'create' | 'update' };
|
|
131
|
+
await route.fulfill({
|
|
132
|
+
contentType: 'application/json',
|
|
133
|
+
json: {
|
|
134
|
+
schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
135
|
+
environment: {
|
|
136
|
+
id: 'field-protocol-environment',
|
|
137
|
+
key: 'preproduction',
|
|
138
|
+
activeAppVersionId: 'field-protocol-version',
|
|
139
|
+
headRevision: 1,
|
|
140
|
+
authzRevisionId: 'field-protocol-authz',
|
|
141
|
+
authzVersion: 1,
|
|
142
|
+
scopeDataVersion: 'field-protocol-scope',
|
|
143
|
+
},
|
|
144
|
+
resourceCode: 'field-records',
|
|
145
|
+
fieldCode,
|
|
146
|
+
sourceResourceCode: 'field-lookups',
|
|
147
|
+
operation: body.operation || 'create',
|
|
148
|
+
items: [{
|
|
149
|
+
label: `候选-${fieldCode}`,
|
|
150
|
+
value: `candidate-${fieldCode}`,
|
|
151
|
+
resourceCode: 'field-lookups',
|
|
152
|
+
snapshot: { code: `code-${fieldCode}` },
|
|
153
|
+
}],
|
|
154
|
+
nextCursor: null,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await page.goto('/field-protocol.e2e.html');
|
|
160
|
+
for (const fieldCode of ['资源单选', '资源多选']) {
|
|
161
|
+
await page.locator(`[data-field-code="${fieldCode}"]`).getByRole('combobox').click();
|
|
162
|
+
await expect.poll(() => requestCounts.get(fieldCode) || 0).toBe(1);
|
|
163
|
+
await page
|
|
164
|
+
.locator('.ant-select-dropdown:visible .oxa-selector-option')
|
|
165
|
+
.filter({ hasText: `候选-${fieldCode}` })
|
|
166
|
+
.click();
|
|
167
|
+
await page.waitForTimeout(500);
|
|
168
|
+
expect(requestCounts.get(fieldCode)).toBe(1);
|
|
169
|
+
await page.keyboard.press('Escape');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
87
173
|
test('renders every mobile control without horizontal overflow', async ({ page }) => {
|
|
88
174
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
89
175
|
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
@@ -21,119 +21,26 @@ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
|
|
|
21
21
|
const allCapabilities = [...capabilities] as string[];
|
|
22
22
|
const runtimeBase = `/view/${appCode}`;
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
subjectKey: 'membership:college-admin',
|
|
27
|
-
subjectKind: 'membership',
|
|
28
|
-
role: {
|
|
29
|
-
code: 'college_admin',
|
|
30
|
-
name: '学院管理员',
|
|
31
|
-
source: 'package',
|
|
32
|
-
description: '管理本学院业务数据',
|
|
33
|
-
},
|
|
34
|
-
revision: 1,
|
|
35
|
-
scopeDimensionCount: 1,
|
|
36
|
-
scopeSummary: [],
|
|
37
|
-
scopeSummaryTruncated: false,
|
|
38
|
-
validFrom: null,
|
|
39
|
-
validTo: null,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
subjectKey: 'membership:instrument-admin',
|
|
43
|
-
subjectKind: 'membership',
|
|
44
|
-
role: {
|
|
45
|
-
code: 'instrument_admin',
|
|
46
|
-
name: '仪器管理员',
|
|
47
|
-
source: 'package',
|
|
48
|
-
description: '管理分配的仪器',
|
|
49
|
-
},
|
|
50
|
-
revision: 1,
|
|
51
|
-
scopeDimensionCount: 0,
|
|
52
|
-
scopeSummary: [],
|
|
53
|
-
scopeSummaryTruncated: false,
|
|
54
|
-
validFrom: null,
|
|
55
|
-
validTo: null,
|
|
56
|
-
},
|
|
57
|
-
] as const;
|
|
58
|
-
|
|
59
|
-
function roleContext(subjectKey: string, authorized: boolean) {
|
|
60
|
-
const subject = roleSubjects.find(item => item.subjectKey === subjectKey)!;
|
|
61
|
-
const roleSessionId = `role-session-${subject.role.code}`;
|
|
24
|
+
function runtimeAuthorization(authorized: boolean) {
|
|
62
25
|
return {
|
|
63
|
-
schemaVersion: 'openxiangda.
|
|
26
|
+
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
64
27
|
state: 'active',
|
|
65
28
|
environment: {
|
|
66
29
|
id: 'preproduction-id',
|
|
67
30
|
key: 'preproduction',
|
|
68
|
-
kind: 'preproduction',
|
|
69
31
|
activeAppVersionId: 'app-version-1',
|
|
70
32
|
headRevision: 1,
|
|
33
|
+
authzRevisionId: 'authz-1',
|
|
71
34
|
authzVersion: 1,
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
identityScope: `preproduction-id:${subjectKey}:1`,
|
|
75
|
-
subjectProfile: {
|
|
76
|
-
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
77
|
-
userId: 'acceptance-user',
|
|
78
|
-
displayName: '验收用户',
|
|
79
|
-
avatarUrl: null,
|
|
80
|
-
jobNumber: null,
|
|
81
|
-
affiliatedDepartment: { id: 'department-1', name: '实验中心' },
|
|
82
|
-
},
|
|
83
|
-
currentRoleSubject: subject,
|
|
84
|
-
roleSubjectPage: {
|
|
85
|
-
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
86
|
-
items: roleSubjects,
|
|
87
|
-
total: roleSubjects.length,
|
|
88
|
-
nextCursor: null,
|
|
89
|
-
roleSubjectSetVersion: 'subjects-1',
|
|
90
|
-
},
|
|
91
|
-
roleSession: {
|
|
92
|
-
schemaVersion: 'openxiangda.native-role-session/v2',
|
|
93
|
-
id: roleSessionId,
|
|
94
|
-
environmentId: 'preproduction-id',
|
|
95
|
-
environmentKey: 'preproduction',
|
|
96
|
-
loginSessionId: 'login-session-1',
|
|
97
|
-
userId: 'acceptance-user',
|
|
98
|
-
selectionKey: subjectKey,
|
|
99
|
-
subjectKind: 'membership',
|
|
100
|
-
activeRoleCode: subject.role.code,
|
|
101
|
-
activeRoleSource: 'package',
|
|
102
|
-
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
103
|
-
activeAuthzRevisionId: 'authz-1',
|
|
104
|
-
activeAuthzVersion: 1,
|
|
105
|
-
activeAppVersionId: 'app-version-1',
|
|
106
|
-
activeEnvironmentHeadRevision: 1,
|
|
107
|
-
activeMembershipRevision: 1,
|
|
108
|
-
status: 'active',
|
|
109
|
-
issuedAt: '2026-08-23T00:00:00.000Z',
|
|
110
|
-
expiresAt: '2099-08-23T00:00:00.000Z',
|
|
111
|
-
lastUsedAt: '2026-08-23T00:00:00.000Z',
|
|
35
|
+
scopeDataVersion: 'scope-1',
|
|
112
36
|
},
|
|
113
37
|
principal: {
|
|
114
|
-
|
|
115
|
-
tenantId: 'tenant-1',
|
|
116
|
-
appCode,
|
|
117
|
-
environmentId: 'preproduction-id',
|
|
118
|
-
environmentKey: 'preproduction',
|
|
119
|
-
activeAppVersionId: 'app-version-1',
|
|
120
|
-
environmentHeadRevision: 1,
|
|
121
|
-
principalType: 'user',
|
|
122
|
-
subjectId: 'acceptance-user',
|
|
38
|
+
type: 'user_union',
|
|
123
39
|
userId: 'acceptance-user',
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
subjectKind: 'membership',
|
|
127
|
-
activeRoleCode: subject.role.code,
|
|
128
|
-
activeRoleSource: 'package',
|
|
129
|
-
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
130
|
-
activeMembershipRevision: 1,
|
|
131
|
-
authzRevisionId: 'authz-1',
|
|
132
|
-
authzVersion: 1,
|
|
133
|
-
scopeDataVersion: 'scope-1',
|
|
40
|
+
roleCodes: ['college_admin', 'instrument_admin'],
|
|
41
|
+
capabilityCodes: authorized ? allCapabilities : [],
|
|
134
42
|
isAppSuperAdmin: false,
|
|
135
|
-
|
|
136
|
-
expiresAt: null,
|
|
43
|
+
identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
|
|
137
44
|
},
|
|
138
45
|
};
|
|
139
46
|
}
|
|
@@ -143,10 +50,8 @@ async function mockPlatform(
|
|
|
143
50
|
authorized = true,
|
|
144
51
|
targetRuntimeBase = runtimeBase,
|
|
145
52
|
) {
|
|
146
|
-
let activeSubjectKey = roleSubjects[0].subjectKey;
|
|
147
53
|
let contextReads = 0;
|
|
148
54
|
const observed: Array<{ path: string; roleSessionId: string }> = [];
|
|
149
|
-
const switches: Array<Record<string, unknown>> = [];
|
|
150
55
|
await page.route(`**${targetRuntimeBase}/**`, async route => {
|
|
151
56
|
if (route.request().resourceType() !== 'document') return route.continue();
|
|
152
57
|
const response = await route.fetch();
|
|
@@ -162,42 +67,13 @@ async function mockPlatform(
|
|
|
162
67
|
const url = new URL(route.request().url());
|
|
163
68
|
const request = route.request();
|
|
164
69
|
const roleSessionId = request.headers()['x-openxiangda-role-session-id'] || '';
|
|
165
|
-
if (url.pathname.endsWith('/native/authz/
|
|
166
|
-
return route.fulfill({
|
|
167
|
-
contentType: 'application/json',
|
|
168
|
-
body: JSON.stringify({
|
|
169
|
-
code: 200,
|
|
170
|
-
data: {
|
|
171
|
-
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
172
|
-
items: roleSubjects,
|
|
173
|
-
total: roleSubjects.length,
|
|
174
|
-
nextCursor: null,
|
|
175
|
-
roleSubjectSetVersion: 'subjects-1',
|
|
176
|
-
},
|
|
177
|
-
}),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
if (url.pathname.endsWith('/native/authz/role-session/context/switch')) {
|
|
181
|
-
const body = request.postDataJSON() as Record<string, unknown>;
|
|
182
|
-
switches.push(body);
|
|
183
|
-
if (roleSubjects.some(item => item.subjectKey === body.targetRoleSubjectKey)) {
|
|
184
|
-
activeSubjectKey = String(body.targetRoleSubjectKey) as typeof activeSubjectKey;
|
|
185
|
-
}
|
|
186
|
-
return route.fulfill({
|
|
187
|
-
contentType: 'application/json',
|
|
188
|
-
body: JSON.stringify({
|
|
189
|
-
code: 200,
|
|
190
|
-
data: roleContext(activeSubjectKey, authorized),
|
|
191
|
-
}),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
if (url.pathname.endsWith('/native/authz/role-session/context')) {
|
|
70
|
+
if (url.pathname.endsWith('/native/authz/current')) {
|
|
195
71
|
contextReads += 1;
|
|
196
72
|
return route.fulfill({
|
|
197
73
|
contentType: 'application/json',
|
|
198
74
|
body: JSON.stringify({
|
|
199
75
|
code: 200,
|
|
200
|
-
data:
|
|
76
|
+
data: runtimeAuthorization(authorized),
|
|
201
77
|
}),
|
|
202
78
|
});
|
|
203
79
|
}
|
|
@@ -262,7 +138,6 @@ async function mockPlatform(
|
|
|
262
138
|
});
|
|
263
139
|
return {
|
|
264
140
|
observed,
|
|
265
|
-
switches,
|
|
266
141
|
get contextReads() {
|
|
267
142
|
return contextReads;
|
|
268
143
|
},
|
|
@@ -314,7 +189,7 @@ test('opens the stable admin entry under the preproduction gateway mount', async
|
|
|
314
189
|
await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
|
|
315
190
|
});
|
|
316
191
|
|
|
317
|
-
test('
|
|
192
|
+
test('uses the current user role union without exposing a RoleSession header', async ({ page }) => {
|
|
318
193
|
const platform = await mockPlatform(page);
|
|
319
194
|
await page.goto(`${runtimeBase}/admin`);
|
|
320
195
|
if (codes.length) {
|
|
@@ -324,7 +199,7 @@ test('binds Data, Directory, App API, and role switching to one active RoleSessi
|
|
|
324
199
|
}
|
|
325
200
|
|
|
326
201
|
await page.evaluate(async () => {
|
|
327
|
-
const client = await import('/
|
|
202
|
+
const client = await import('/e2e/client-fixture.ts');
|
|
328
203
|
await client.searchResource('acceptance-resource', 'name', {
|
|
329
204
|
keyword: '显微镜',
|
|
330
205
|
});
|
|
@@ -332,55 +207,19 @@ test('binds Data, Directory, App API, and role switching to one active RoleSessi
|
|
|
332
207
|
await client.requestApplicationApi('/health/current-user');
|
|
333
208
|
});
|
|
334
209
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
/\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
|
|
339
|
-
item.path
|
|
340
|
-
)
|
|
210
|
+
const protectedRequests = platform.observed.filter(item =>
|
|
211
|
+
/\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
|
|
212
|
+
item.path
|
|
341
213
|
)
|
|
342
|
-
).toEqual(
|
|
343
|
-
expect.arrayContaining([
|
|
344
|
-
expect.objectContaining({ roleSessionId: initialSession }),
|
|
345
|
-
expect.objectContaining({ roleSessionId: initialSession }),
|
|
346
|
-
expect.objectContaining({ roleSessionId: initialSession }),
|
|
347
|
-
])
|
|
348
214
|
);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
await page.getByRole('combobox', { name: '选择应用角色' }).click();
|
|
353
|
-
await page.getByText('仪器管理员', { exact: true }).click();
|
|
354
|
-
await page.getByRole('button', { name: '切换角色' }).click();
|
|
355
|
-
|
|
356
|
-
await expect.poll(() => platform.switches.length).toBe(1);
|
|
357
|
-
expect(platform.switches[0]).toMatchObject({
|
|
358
|
-
targetRoleSubjectKey: 'membership:instrument-admin',
|
|
359
|
-
expectedRoleSessionId: initialSession,
|
|
360
|
-
environmentKey: 'preproduction',
|
|
361
|
-
});
|
|
362
|
-
await expect.poll(() => platform.contextReads).toBeGreaterThan(1);
|
|
215
|
+
expect(protectedRequests.length).toBeGreaterThanOrEqual(3);
|
|
216
|
+
expect(protectedRequests.every(item => item.roleSessionId === '')).toBe(true);
|
|
217
|
+
expect(platform.contextReads).toBe(1);
|
|
363
218
|
await page.locator('.oxa-current-user').click();
|
|
364
219
|
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
365
|
-
'
|
|
220
|
+
'college_admin、instrument_admin'
|
|
366
221
|
);
|
|
367
|
-
await page.
|
|
368
|
-
await page.evaluate(async () => {
|
|
369
|
-
const client = await import('/src/platform-client.ts');
|
|
370
|
-
await client.searchResource('acceptance-resource', 'name', {
|
|
371
|
-
keyword: '光谱仪',
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
await expect
|
|
375
|
-
.poll(
|
|
376
|
-
() =>
|
|
377
|
-
platform.observed.filter(
|
|
378
|
-
item =>
|
|
379
|
-
item.path.includes('/native/data-resources/') &&
|
|
380
|
-
item.roleSessionId === 'role-session-instrument_admin'
|
|
381
|
-
).length
|
|
382
|
-
)
|
|
383
|
-
.toBeGreaterThan(0);
|
|
222
|
+
await expect(page.getByText('切换应用角色', { exact: true })).toHaveCount(0);
|
|
384
223
|
});
|
|
385
224
|
|
|
386
225
|
test('renders platform mobile controls from declared field widgets', async ({ page }) => {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
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="field-protocol-fixture" />
|
|
8
|
+
<meta name="openxiangda-environment" content="preproduction" />
|
|
6
9
|
<title>Field protocol acceptance</title>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
"@ant-design/icons": "6.2.3",
|
|
15
15
|
"@refinedev/core": "5.0.12",
|
|
16
16
|
"antd": "6.4.3",
|
|
17
|
-
"
|
|
18
|
-
"docx-preview": "0.3.7",
|
|
19
|
-
"openxiangda-contracts": "2.0.0-alpha.43",
|
|
17
|
+
"openxiangda": "2.0.0-alpha.2",
|
|
20
18
|
"react": "19.2.8",
|
|
21
19
|
"react-dom": "19.2.8",
|
|
22
|
-
"react-router-dom": "7.8.2"
|
|
23
|
-
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
20
|
+
"react-router-dom": "7.8.2"
|
|
24
21
|
},
|
|
25
22
|
"devDependencies": {
|
|
26
23
|
"@playwright/test": "1.62.1",
|
|
@@ -1,67 +1,19 @@
|
|
|
1
|
-
import { Refine } from '@refinedev/core';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
import ReactDOM from 'react-dom/client';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import { RuntimeBoundary } from './runtime';
|
|
12
|
-
import { applicationBasename } from './runtime-meta';
|
|
13
|
-
import { useGlobalRequestLoading } from './platform-client';
|
|
14
|
-
import './styles.css';
|
|
15
|
-
|
|
16
|
-
const codes = Object.keys(resourceDefinitions);
|
|
17
|
-
const firstPath = codes[0] ? `/${codes[0]}` : '/';
|
|
18
|
-
const adminEntry = codes.length ? (
|
|
19
|
-
<Navigate replace to={firstPath} />
|
|
20
|
-
) : (
|
|
21
|
-
<EmptyApplicationPage />
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
function GlobalRequestLoading() {
|
|
25
|
-
const loading = useGlobalRequestLoading();
|
|
26
|
-
return loading ? (
|
|
27
|
-
<div
|
|
28
|
-
aria-label="页面请求进行中"
|
|
29
|
-
aria-valuetext="正在加载"
|
|
30
|
-
className="oxa-global-request-loading"
|
|
31
|
-
role="progressbar"
|
|
32
|
-
>
|
|
33
|
-
<span />
|
|
34
|
-
</div>
|
|
35
|
-
) : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const resourceRoutes = codes.flatMap(resourceCode => [
|
|
39
|
-
<Route key={`${resourceCode}-list`} path={`/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} />} />,
|
|
40
|
-
<Route key={`${resourceCode}-new`} path={`/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} />} />,
|
|
41
|
-
<Route key={`${resourceCode}-detail`} path={`/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} />} />,
|
|
42
|
-
<Route key={`${resourceCode}-edit`} path={`/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} />} />,
|
|
43
|
-
<Route key={`${resourceCode}-mobile-list`} path={`/m/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} variant="mobile" />} />,
|
|
44
|
-
<Route key={`${resourceCode}-mobile-new`} path={`/m/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} variant="mobile" />} />,
|
|
45
|
-
<Route key={`${resourceCode}-mobile-detail`} path={`/m/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} variant="mobile" />} />,
|
|
46
|
-
<Route key={`${resourceCode}-mobile-edit`} path={`/m/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} variant="mobile" />} />,
|
|
47
|
-
]);
|
|
3
|
+
import { OpenXiangdaApplication } from 'openxiangda/react';
|
|
4
|
+
import {
|
|
5
|
+
appCode,
|
|
6
|
+
appName,
|
|
7
|
+
resourceDefinitions,
|
|
8
|
+
} from '../../../packages/contracts/src/generated.js';
|
|
9
|
+
import 'openxiangda/react/styles.css';
|
|
48
10
|
|
|
49
11
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
50
12
|
<React.StrictMode>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<Routes>
|
|
57
|
-
<Route path="/" element={adminEntry} />
|
|
58
|
-
<Route path="/admin" element={adminEntry} />
|
|
59
|
-
<Route path="/files/:resourceCode/:fileId/preview" element={<FilePreviewPage />} />
|
|
60
|
-
{resourceRoutes}
|
|
61
|
-
</Routes>
|
|
62
|
-
</Refine>
|
|
63
|
-
</BrowserRouter>
|
|
64
|
-
</RuntimeBoundary>
|
|
65
|
-
</AppearanceProvider>
|
|
13
|
+
<OpenXiangdaApplication
|
|
14
|
+
appCode={appCode}
|
|
15
|
+
appName={appName}
|
|
16
|
+
resourceDefinitions={resourceDefinitions}
|
|
17
|
+
/>
|
|
66
18
|
</React.StrictMode>,
|
|
67
19
|
);
|