openxiangda-cli 2.0.0-alpha.57 → 2.0.0-alpha.59
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/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +13 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/create-workspace.d.ts +6 -1
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +12 -9
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/README.md +3 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +1197 -9
- package/template/apps/web/package.json +4 -2
- package/template/apps/web/scripts/check.mjs +2 -2
- package/template/apps/web/scripts/verify-build.mjs +14 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
- package/template/apps/web/src/CollegePage.tsx +181 -0
- package/template/apps/web/src/FilePreviewPage.tsx +303 -0
- package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
- package/template/apps/web/src/InstrumentForm.tsx +347 -94
- package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
- package/template/apps/web/src/InstrumentListPage.tsx +30 -35
- package/template/apps/web/src/Shell.tsx +229 -41
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +226 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
- package/template/apps/web/src/data-provider.ts +1 -1
- package/template/apps/web/src/fields.ts +17 -6
- package/template/apps/web/src/instrument.ts +1 -1
- package/template/apps/web/src/main.tsx +21 -1
- package/template/apps/web/src/platform-client.ts +248 -2
- package/template/apps/web/src/runtime-meta.ts +15 -0
- package/template/apps/web/src/runtime.tsx +4 -2
- package/template/apps/web/src/styles.css +1007 -28
- package/template/apps/web/test/contracts.test.ts +473 -10
- package/template/openxiangda.config.ts +24 -5
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +22 -0
- package/template/platform/data/colleges.ts +21 -0
- package/template/platform/data/instruments.ts +1 -1
- package/template/scripts/verify-template-budget.mjs +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import test from 'node:test';
|
|
3
4
|
import {
|
|
4
5
|
instrumentFieldAllowed,
|
|
@@ -6,16 +7,28 @@ import {
|
|
|
6
7
|
} from '../src/InstrumentForm';
|
|
7
8
|
import { instrumentFields } from '../src/fields';
|
|
8
9
|
import {
|
|
10
|
+
browseDepartmentTree,
|
|
11
|
+
browseDepartmentUsers,
|
|
9
12
|
instrumentDataApi,
|
|
13
|
+
fetchDataFileBlob,
|
|
14
|
+
loadDataFilePreview,
|
|
10
15
|
loadCurrentIdentity,
|
|
16
|
+
resolveCollegeScope,
|
|
17
|
+
resolveDirectory,
|
|
18
|
+
searchCollegeScope,
|
|
19
|
+
searchDirectory,
|
|
11
20
|
} from '../src/platform-client';
|
|
12
21
|
import {
|
|
13
22
|
applicationApiPath,
|
|
14
23
|
readRuntimeMount,
|
|
24
|
+
resolveAttachmentPreviewPath,
|
|
15
25
|
resolveApplicationApiPath,
|
|
16
26
|
resolveApplicationBasename,
|
|
17
27
|
} from '../src/runtime-meta';
|
|
18
28
|
|
|
29
|
+
const collegeId = '11111111-1111-4111-8111-111111111111';
|
|
30
|
+
const userId = '22222222-2222-4222-8222-222222222222';
|
|
31
|
+
|
|
19
32
|
test('the golden module exposes exactly 30 fields and five restricted fields', () => {
|
|
20
33
|
assert.equal(instrumentFields.length, 30);
|
|
21
34
|
assert.deepEqual(
|
|
@@ -36,6 +49,102 @@ test('the golden module exposes exactly 30 fields and five restricted fields', (
|
|
|
36
49
|
);
|
|
37
50
|
});
|
|
38
51
|
|
|
52
|
+
test('application shell and instrument surfaces match the approved navigation model', () => {
|
|
53
|
+
const shell = readFileSync(new URL('../src/Shell.tsx', import.meta.url), 'utf8');
|
|
54
|
+
const list = readFileSync(
|
|
55
|
+
new URL('../src/InstrumentListPage.tsx', import.meta.url),
|
|
56
|
+
'utf8'
|
|
57
|
+
);
|
|
58
|
+
const routes = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
|
|
59
|
+
const form = readFileSync(
|
|
60
|
+
new URL('../src/InstrumentForm.tsx', import.meta.url),
|
|
61
|
+
'utf8'
|
|
62
|
+
);
|
|
63
|
+
const colleges = readFileSync(
|
|
64
|
+
new URL('../src/CollegePage.tsx', import.meta.url),
|
|
65
|
+
'utf8'
|
|
66
|
+
);
|
|
67
|
+
const directory = readFileSync(
|
|
68
|
+
new URL('../src/components/platform-fields/PlatformDirectoryPicker.tsx', import.meta.url),
|
|
69
|
+
'utf8'
|
|
70
|
+
);
|
|
71
|
+
const attachment = readFileSync(
|
|
72
|
+
new URL('../src/components/platform-fields/AttachmentFileList.tsx', import.meta.url),
|
|
73
|
+
'utf8'
|
|
74
|
+
);
|
|
75
|
+
const preview = readFileSync(
|
|
76
|
+
new URL('../src/FilePreviewPage.tsx', import.meta.url),
|
|
77
|
+
'utf8'
|
|
78
|
+
);
|
|
79
|
+
for (const marker of [
|
|
80
|
+
'仪器资源管理',
|
|
81
|
+
'工作台',
|
|
82
|
+
'仪器管理',
|
|
83
|
+
'仪器资源',
|
|
84
|
+
'开放记录',
|
|
85
|
+
'基础数据',
|
|
86
|
+
'学院字典',
|
|
87
|
+
'系统设置',
|
|
88
|
+
'应用管理员',
|
|
89
|
+
'收起侧边栏',
|
|
90
|
+
]) {
|
|
91
|
+
assert.match(shell, new RegExp(marker));
|
|
92
|
+
}
|
|
93
|
+
assert.doesNotMatch(shell, /<span>\{identity\.userId\}<\/span>/);
|
|
94
|
+
assert.doesNotMatch(shell, /验证本地 Nest|roleCodes\.join|environment\.key/);
|
|
95
|
+
assert.doesNotMatch(list, /<Drawer|surface="drawer"/);
|
|
96
|
+
assert.match(routes, /path="\/instruments\/new"[\s\S]*<InstrumentFormPage mode="create"/);
|
|
97
|
+
assert.match(colleges, /<Drawer/);
|
|
98
|
+
assert.match(colleges, /size=\{520\}/);
|
|
99
|
+
assert.match(directory, /width=\{kind === 'user' \? 980 : 760\}/);
|
|
100
|
+
assert.match(directory, /browseDepartmentTree|browseDepartmentUsers/);
|
|
101
|
+
assert.match(directory, /选择成员|选择部门|组织架构|已选/);
|
|
102
|
+
assert.match(form, /oxa-form-full/);
|
|
103
|
+
assert.match(form, /PlatformDirectoryPicker/);
|
|
104
|
+
assert.match(form, /oxa-file-dropzone/);
|
|
105
|
+
assert.match(form, /oxa-detail-layout/);
|
|
106
|
+
assert.match(routes, /path="\/files\/:resourceCode\/:fileId\/preview"/);
|
|
107
|
+
assert.match(attachment, /Image\.PreviewGroup/);
|
|
108
|
+
assert.match(attachment, /attachmentPreviewPath/);
|
|
109
|
+
assert.match(preview, /import\('docx-preview'\)/);
|
|
110
|
+
assert.match(preview, /import\('xlsx'\)/);
|
|
111
|
+
assert.match(preview, /只读预览/);
|
|
112
|
+
assert.doesNotMatch(form, /instrumentFileContentUrl|window\.open/);
|
|
113
|
+
assert.doesNotMatch(
|
|
114
|
+
`${form}\n${directory}`,
|
|
115
|
+
/FormContext|tools\/openxiangda|RoleSession/
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('college, user, and department selectors have authoritative owners only', () => {
|
|
120
|
+
const appConfig = readFileSync(
|
|
121
|
+
new URL('../../../openxiangda.config.ts', import.meta.url),
|
|
122
|
+
'utf8'
|
|
123
|
+
);
|
|
124
|
+
const colleges = readFileSync(
|
|
125
|
+
new URL('../../../platform/data/colleges.ts', import.meta.url),
|
|
126
|
+
'utf8'
|
|
127
|
+
);
|
|
128
|
+
const fields = readFileSync(new URL('../src/fields.ts', import.meta.url), 'utf8');
|
|
129
|
+
assert.match(appConfig, /valueType:\s*'uuid'/);
|
|
130
|
+
assert.match(appConfig, /kind:\s*'native_resource'/);
|
|
131
|
+
assert.match(appConfig, /resourceCode:\s*'colleges'/);
|
|
132
|
+
assert.match(appConfig, /data:\s*\{\s*resources:\s*\[colleges, instruments\]/);
|
|
133
|
+
assert.match(colleges, /code:\s*'name'.*type:\s*'string'/s);
|
|
134
|
+
assert.match(colleges, /code:\s*'enabled'.*type:\s*'boolean'/s);
|
|
135
|
+
assert.match(fields, /kind:\s*'scope'/);
|
|
136
|
+
assert.match(fields, /kind:\s*'directory-user'/);
|
|
137
|
+
assert.match(fields, /kind:\s*'directory-department'/);
|
|
138
|
+
for (const marker of [
|
|
139
|
+
['college', 'am'].join('-'),
|
|
140
|
+
['user', 'wang'].join('-'),
|
|
141
|
+
['dept', 'am-test'].join('-'),
|
|
142
|
+
]) {
|
|
143
|
+
assert.equal(appConfig.includes(marker), false);
|
|
144
|
+
assert.equal(fields.includes(marker), false);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
39
148
|
test('college create restores five fields while update omits college reassignment', () => {
|
|
40
149
|
const protectedFields = instrumentFields.filter(
|
|
41
150
|
field => field.createCapability && field.updateCapability
|
|
@@ -53,8 +162,8 @@ test('college create restores five fields while update omits college reassignmen
|
|
|
53
162
|
chineseName: '电子显微镜',
|
|
54
163
|
assetCode: 'ASSET-001',
|
|
55
164
|
assetValue: 100,
|
|
56
|
-
collegeId
|
|
57
|
-
instrumentAdminIds: [
|
|
165
|
+
collegeId,
|
|
166
|
+
instrumentAdminIds: [userId],
|
|
58
167
|
revision: 7,
|
|
59
168
|
};
|
|
60
169
|
assert.equal(
|
|
@@ -78,15 +187,15 @@ test('college create restores five fields while update omits college reassignmen
|
|
|
78
187
|
chineseName: '电子显微镜',
|
|
79
188
|
assetCode: 'ASSET-001',
|
|
80
189
|
assetValue: 100,
|
|
81
|
-
collegeId
|
|
82
|
-
instrumentAdminIds: [
|
|
190
|
+
collegeId,
|
|
191
|
+
instrumentAdminIds: [userId],
|
|
83
192
|
});
|
|
84
193
|
assert.deepEqual(sanitizeInstrumentValues(values, 'edit', hasCapability), {
|
|
85
194
|
instrumentCode: 'INS-001',
|
|
86
195
|
chineseName: '电子显微镜',
|
|
87
196
|
assetCode: 'ASSET-001',
|
|
88
197
|
assetValue: 100,
|
|
89
|
-
instrumentAdminIds: [
|
|
198
|
+
instrumentAdminIds: [userId],
|
|
90
199
|
});
|
|
91
200
|
});
|
|
92
201
|
|
|
@@ -162,6 +271,92 @@ test('deployed app/environment come only from platform-injected runtime meta', (
|
|
|
162
271
|
resolveApplicationApiPath(mount, 'api/instruments/context'),
|
|
163
272
|
'/service/openxiangda-app-api/v2/instrument-center/production/api/instruments/context'
|
|
164
273
|
);
|
|
274
|
+
assert.equal(
|
|
275
|
+
resolveAttachmentPreviewPath(
|
|
276
|
+
mount,
|
|
277
|
+
'instruments',
|
|
278
|
+
'77777777-7777-4777-8777-777777777777'
|
|
279
|
+
),
|
|
280
|
+
'/view/instrument-center/files/instruments/77777777-7777-4777-8777-777777777777/preview'
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('managed file preview reads metadata and bytes without a RoleSession', async () => {
|
|
285
|
+
const calls: Array<{ path: string; init?: RequestInit }> = [];
|
|
286
|
+
const originalFetch = globalThis.fetch;
|
|
287
|
+
globalThis.fetch = async (input, init) => {
|
|
288
|
+
const path = String(input);
|
|
289
|
+
calls.push({ path, init });
|
|
290
|
+
if (path.endsWith('/dev-sessions/current')) {
|
|
291
|
+
return Response.json({
|
|
292
|
+
code: 200,
|
|
293
|
+
data: {
|
|
294
|
+
environment: { id: 'test-id', key: 'preproduction' },
|
|
295
|
+
principal: {
|
|
296
|
+
userId,
|
|
297
|
+
roleCodes: ['school_admin'],
|
|
298
|
+
capabilityCodes: [],
|
|
299
|
+
isAppSuperAdmin: false,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (path.includes('/files/file-1/preview?')) {
|
|
305
|
+
return Response.json({
|
|
306
|
+
code: 200,
|
|
307
|
+
data: {
|
|
308
|
+
schemaVersion: 'openxiangda.data-file-preview/v2',
|
|
309
|
+
resourceCode: 'instruments',
|
|
310
|
+
file: {
|
|
311
|
+
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
312
|
+
id: 'file-1',
|
|
313
|
+
name: 'manual.docx',
|
|
314
|
+
size: 12,
|
|
315
|
+
contentType:
|
|
316
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
317
|
+
},
|
|
318
|
+
extension: 'docx',
|
|
319
|
+
previewType: 'office',
|
|
320
|
+
renderMode: 'docx-html',
|
|
321
|
+
previewSurface: 'document',
|
|
322
|
+
previewProvider: 'browser',
|
|
323
|
+
canPreview: true,
|
|
324
|
+
canDownload: true,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (path.includes('/files/file-1/content?')) {
|
|
329
|
+
return new Response(new Uint8Array([80, 75, 3, 4]), {
|
|
330
|
+
status: 200,
|
|
331
|
+
headers: {
|
|
332
|
+
'content-type':
|
|
333
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
await loadCurrentIdentity();
|
|
341
|
+
const preview = await loadDataFilePreview('instruments', 'file-1');
|
|
342
|
+
const blob = await fetchDataFileBlob('instruments', 'file-1');
|
|
343
|
+
assert.equal(preview.renderMode, 'docx-html');
|
|
344
|
+
assert.equal(blob.size, 4);
|
|
345
|
+
} finally {
|
|
346
|
+
globalThis.fetch = originalFetch;
|
|
347
|
+
}
|
|
348
|
+
const previewCall = calls.find(call => call.path.includes('/files/file-1/preview?'))!;
|
|
349
|
+
const contentCall = calls.find(call => call.path.includes('/files/file-1/content?'))!;
|
|
350
|
+
assert.match(previewCall.path, /environmentKey=preproduction/);
|
|
351
|
+
assert.match(contentCall.path, /disposition=inline/);
|
|
352
|
+
assert.equal(
|
|
353
|
+
calls.some(call =>
|
|
354
|
+
Object.keys(call.init?.headers || {}).some(header =>
|
|
355
|
+
header.toLowerCase().includes('role-session')
|
|
356
|
+
)
|
|
357
|
+
),
|
|
358
|
+
false
|
|
359
|
+
);
|
|
165
360
|
});
|
|
166
361
|
|
|
167
362
|
test('list uses the native Data API with server paging, filters, and sorting', async () => {
|
|
@@ -206,29 +401,297 @@ test('list uses the native Data API with server paging, filters, and sorting', a
|
|
|
206
401
|
await instrumentDataApi.list({
|
|
207
402
|
page: 2,
|
|
208
403
|
pageSize: 20,
|
|
209
|
-
collegeId
|
|
210
|
-
instrumentAdminId:
|
|
404
|
+
collegeId,
|
|
405
|
+
instrumentAdminId: userId,
|
|
211
406
|
sort: { field: 'assetValue', order: 'asc' },
|
|
212
407
|
});
|
|
408
|
+
await instrumentDataApi.list({ page: 1, pageSize: 20 });
|
|
213
409
|
} finally {
|
|
214
410
|
globalThis.fetch = originalFetch;
|
|
215
411
|
}
|
|
216
|
-
assert.equal(calls.length,
|
|
412
|
+
assert.equal(calls.length, 3);
|
|
217
413
|
assert.match(
|
|
218
414
|
calls[1]!.path,
|
|
219
415
|
/\/native\/data\/instruments\/query$/
|
|
220
416
|
);
|
|
221
417
|
const body = JSON.parse(String(calls[1]!.init?.body));
|
|
418
|
+
const resourceSource = readFileSync(
|
|
419
|
+
new URL('../../../platform/data/instruments.ts', import.meta.url),
|
|
420
|
+
'utf8'
|
|
421
|
+
);
|
|
422
|
+
const declaredFields = new Set(
|
|
423
|
+
[...resourceSource.matchAll(/\{\s*code:\s*'([^']+)'/g)].map(
|
|
424
|
+
match => match[1]!
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
assert.deepEqual(
|
|
428
|
+
[...declaredFields],
|
|
429
|
+
instrumentFields.filter(field => !field.system).map(field => field.key)
|
|
430
|
+
);
|
|
222
431
|
assert.equal(body.environmentKey, 'preproduction');
|
|
223
432
|
assert.equal(body.limit, 20);
|
|
224
433
|
assert.equal(body.offset, 20);
|
|
225
434
|
assert.deepEqual(body.order, [{ field: 'assetValue', direction: 'asc' }]);
|
|
435
|
+
assert.equal(
|
|
436
|
+
[...body.filters, ...body.order].every(item =>
|
|
437
|
+
declaredFields.has(item.field)
|
|
438
|
+
),
|
|
439
|
+
true
|
|
440
|
+
);
|
|
226
441
|
assert.deepEqual(body.filters, [
|
|
227
|
-
{ field: 'collegeId', operator: 'eq', value:
|
|
442
|
+
{ field: 'collegeId', operator: 'eq', value: collegeId },
|
|
228
443
|
{
|
|
229
444
|
field: 'instrumentAdminIds',
|
|
230
445
|
operator: 'cs',
|
|
231
|
-
value: [
|
|
446
|
+
value: [userId],
|
|
232
447
|
},
|
|
233
448
|
]);
|
|
449
|
+
const defaultBody = JSON.parse(String(calls[2]!.init?.body));
|
|
450
|
+
assert.deepEqual(defaultBody.order, [
|
|
451
|
+
{ field: 'instrumentCode', direction: 'asc' },
|
|
452
|
+
]);
|
|
453
|
+
assert.equal(
|
|
454
|
+
[...defaultBody.filters, ...defaultBody.order].every(item =>
|
|
455
|
+
declaredFields.has(item.field)
|
|
456
|
+
),
|
|
457
|
+
true
|
|
458
|
+
);
|
|
234
459
|
});
|
|
460
|
+
|
|
461
|
+
test('list rejects an undeclared sorter before issuing a Data API request', async () => {
|
|
462
|
+
let requests = 0;
|
|
463
|
+
const originalFetch = globalThis.fetch;
|
|
464
|
+
globalThis.fetch = async () => {
|
|
465
|
+
requests += 1;
|
|
466
|
+
throw new Error('unexpected fetch');
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
await assert.rejects(
|
|
470
|
+
instrumentDataApi.list({
|
|
471
|
+
page: 1,
|
|
472
|
+
pageSize: 20,
|
|
473
|
+
sort: { field: 'updatedAt', order: 'desc' },
|
|
474
|
+
}),
|
|
475
|
+
/OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:updatedAt/
|
|
476
|
+
);
|
|
477
|
+
} finally {
|
|
478
|
+
globalThis.fetch = originalFetch;
|
|
479
|
+
}
|
|
480
|
+
assert.equal(requests, 0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('authoritative selectors page, resolve exact IDs, and never send RoleSession', async () => {
|
|
484
|
+
const calls: Array<{ path: string; init?: RequestInit }> = [];
|
|
485
|
+
const originalFetch = globalThis.fetch;
|
|
486
|
+
globalThis.fetch = async (input, init) => {
|
|
487
|
+
const path = String(input);
|
|
488
|
+
calls.push({ path, init });
|
|
489
|
+
if (path.endsWith('/dev-sessions/current')) {
|
|
490
|
+
return Response.json({
|
|
491
|
+
code: 200,
|
|
492
|
+
data: {
|
|
493
|
+
environment: {
|
|
494
|
+
id: 'test-id',
|
|
495
|
+
key: 'preproduction',
|
|
496
|
+
activeAppVersionId: 'version-1',
|
|
497
|
+
headRevision: 2,
|
|
498
|
+
authzRevisionId: 'authz-1',
|
|
499
|
+
authzVersion: 3,
|
|
500
|
+
scopeDataVersion: 'scope-1',
|
|
501
|
+
},
|
|
502
|
+
principal: {
|
|
503
|
+
userId,
|
|
504
|
+
roleCodes: ['school_admin'],
|
|
505
|
+
capabilityCodes: [],
|
|
506
|
+
isAppSuperAdmin: false,
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (path.includes('/directory/users?')) {
|
|
512
|
+
return Response.json({
|
|
513
|
+
code: 200,
|
|
514
|
+
data: {
|
|
515
|
+
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
516
|
+
kind: 'user',
|
|
517
|
+
items: [
|
|
518
|
+
{ kind: 'user', id: userId, label: '王老师', selectable: true },
|
|
519
|
+
],
|
|
520
|
+
nextCursor: 'next-user-page',
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (path.includes('/directory/departments/tree?')) {
|
|
525
|
+
return Response.json({
|
|
526
|
+
code: 200,
|
|
527
|
+
data: {
|
|
528
|
+
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
529
|
+
kind: 'department',
|
|
530
|
+
items: [
|
|
531
|
+
{
|
|
532
|
+
kind: 'department',
|
|
533
|
+
id: 'department-root',
|
|
534
|
+
label: '材料学院',
|
|
535
|
+
hasChildren: true,
|
|
536
|
+
selectable: true,
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
nextCursor: null,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if (path.includes('/directory/departments/department-root/users?')) {
|
|
544
|
+
return Response.json({
|
|
545
|
+
code: 200,
|
|
546
|
+
data: {
|
|
547
|
+
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
548
|
+
kind: 'user',
|
|
549
|
+
items: [
|
|
550
|
+
{ kind: 'user', id: userId, label: '王老师', selectable: true },
|
|
551
|
+
],
|
|
552
|
+
nextCursor: null,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (path.includes('/directory/resolve?')) {
|
|
557
|
+
const body = JSON.parse(String(init?.body));
|
|
558
|
+
return Response.json({
|
|
559
|
+
code: 200,
|
|
560
|
+
data: {
|
|
561
|
+
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
562
|
+
kind: body.kind,
|
|
563
|
+
items: body.ids.map((id: string) => ({
|
|
564
|
+
kind: body.kind,
|
|
565
|
+
id,
|
|
566
|
+
label: '精确解析',
|
|
567
|
+
selectable: false,
|
|
568
|
+
})),
|
|
569
|
+
nextCursor: null,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (path.includes('/scope-values/resolve?')) {
|
|
574
|
+
const body = JSON.parse(String(init?.body));
|
|
575
|
+
return Response.json({
|
|
576
|
+
code: 200,
|
|
577
|
+
data: scopePage(
|
|
578
|
+
body.operation,
|
|
579
|
+
body.values.map((value: string) => ({
|
|
580
|
+
value,
|
|
581
|
+
label: '先进材料学院',
|
|
582
|
+
selectable: false,
|
|
583
|
+
}))
|
|
584
|
+
),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (path.includes('/scope-values?')) {
|
|
588
|
+
return Response.json({
|
|
589
|
+
code: 200,
|
|
590
|
+
data: scopePage('create', [
|
|
591
|
+
{ value: collegeId, label: '先进材料学院', selectable: true },
|
|
592
|
+
]),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
|
|
596
|
+
};
|
|
597
|
+
try {
|
|
598
|
+
await loadCurrentIdentity();
|
|
599
|
+
const users = await searchDirectory('user', {
|
|
600
|
+
keyword: '王老',
|
|
601
|
+
cursor: 'cursor-1',
|
|
602
|
+
});
|
|
603
|
+
assert.equal(users.nextCursor, 'next-user-page');
|
|
604
|
+
assert.equal((await resolveDirectory('department', [userId])).items[0]?.selectable, false);
|
|
605
|
+
assert.equal(
|
|
606
|
+
(await browseDepartmentTree({ parentId: 'department-parent' })).items[0]
|
|
607
|
+
?.id,
|
|
608
|
+
'department-root'
|
|
609
|
+
);
|
|
610
|
+
assert.equal(
|
|
611
|
+
(await browseDepartmentUsers('department-root', 2)).items[0]?.id,
|
|
612
|
+
userId
|
|
613
|
+
);
|
|
614
|
+
assert.equal(
|
|
615
|
+
(await searchCollegeScope('create', { keyword: '', cursor: 'scope-1' }))
|
|
616
|
+
.items[0]?.value,
|
|
617
|
+
collegeId
|
|
618
|
+
);
|
|
619
|
+
assert.equal(
|
|
620
|
+
(await resolveCollegeScope('update', [collegeId])).items[0]?.selectable,
|
|
621
|
+
false
|
|
622
|
+
);
|
|
623
|
+
await assert.rejects(
|
|
624
|
+
resolveDirectory('user', []),
|
|
625
|
+
/OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED/
|
|
626
|
+
);
|
|
627
|
+
await assert.rejects(
|
|
628
|
+
resolveCollegeScope('update', []),
|
|
629
|
+
/OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED/
|
|
630
|
+
);
|
|
631
|
+
await assert.rejects(
|
|
632
|
+
resolveDirectory(
|
|
633
|
+
'user',
|
|
634
|
+
Array.from({ length: 51 }, (_, index) => `user-${index}`)
|
|
635
|
+
),
|
|
636
|
+
/OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED/
|
|
637
|
+
);
|
|
638
|
+
await assert.rejects(
|
|
639
|
+
resolveCollegeScope(
|
|
640
|
+
'update',
|
|
641
|
+
Array.from({ length: 51 }, (_, index) =>
|
|
642
|
+
`${String(index).padStart(8, '0')}-0000-4000-8000-000000000000`
|
|
643
|
+
)
|
|
644
|
+
),
|
|
645
|
+
/OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED/
|
|
646
|
+
);
|
|
647
|
+
} finally {
|
|
648
|
+
globalThis.fetch = originalFetch;
|
|
649
|
+
}
|
|
650
|
+
const userSearch = calls.find(call => call.path.includes('/directory/users?'))!;
|
|
651
|
+
const treeBrowse = calls.find(call => call.path.includes('/directory/departments/tree?'))!;
|
|
652
|
+
const memberBrowse = calls.find(call => call.path.includes('/directory/departments/department-root/users?'))!;
|
|
653
|
+
const scopeSearch = calls.find(call => call.path.includes('/scope-values?'))!;
|
|
654
|
+
assert.match(userSearch.path, /keyword=%E7%8E%8B%E8%80%81/);
|
|
655
|
+
assert.match(userSearch.path, /cursor=cursor-1/);
|
|
656
|
+
assert.match(treeBrowse.path, /parentId=department-parent/);
|
|
657
|
+
assert.match(memberBrowse.path, /page=2/);
|
|
658
|
+
assert.match(scopeSearch.path, /environmentKey=preproduction/);
|
|
659
|
+
assert.match(
|
|
660
|
+
scopeSearch.path,
|
|
661
|
+
/\/native\/data-resources\/instruments\/fields\/collegeId\/scope-values\?/
|
|
662
|
+
);
|
|
663
|
+
assert.match(scopeSearch.path, /operation=create/);
|
|
664
|
+
assert.match(scopeSearch.path, /cursor=scope-1/);
|
|
665
|
+
assert.equal(
|
|
666
|
+
calls.some(call =>
|
|
667
|
+
Object.keys(call.init?.headers || {}).some(header =>
|
|
668
|
+
header.toLowerCase().includes('role-session')
|
|
669
|
+
)
|
|
670
|
+
),
|
|
671
|
+
false
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
function scopePage(
|
|
676
|
+
operation: 'create' | 'update',
|
|
677
|
+
items: Array<{ value: string; label: string; selectable: boolean }>
|
|
678
|
+
) {
|
|
679
|
+
return {
|
|
680
|
+
schemaVersion: 'openxiangda.native-scope-value-page/v2',
|
|
681
|
+
environment: {
|
|
682
|
+
id: 'test-id',
|
|
683
|
+
key: 'preproduction',
|
|
684
|
+
activeAppVersionId: 'version-1',
|
|
685
|
+
headRevision: 2,
|
|
686
|
+
authzRevisionId: 'authz-1',
|
|
687
|
+
authzVersion: 3,
|
|
688
|
+
scopeDataVersion: 'scope-1',
|
|
689
|
+
},
|
|
690
|
+
resourceCode: 'instruments',
|
|
691
|
+
fieldCode: 'collegeId',
|
|
692
|
+
dimensionCode: 'college',
|
|
693
|
+
operation,
|
|
694
|
+
items,
|
|
695
|
+
nextCursor: null,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
@@ -3,12 +3,17 @@ import {
|
|
|
3
3
|
instruments,
|
|
4
4
|
protectedFieldCapabilities,
|
|
5
5
|
} from './platform/data/instruments.js';
|
|
6
|
+
import { colleges } from './platform/data/colleges.js';
|
|
6
7
|
|
|
7
8
|
const read = 'app:instrument-center:data:instruments:read';
|
|
8
9
|
const create = 'app:instrument-center:data:instruments:create';
|
|
9
10
|
const update = 'app:instrument-center:data:instruments:update';
|
|
10
11
|
const remove = 'app:instrument-center:data:instruments:delete';
|
|
11
12
|
const inspectContext = 'app:instrument-center:instrument:context:read';
|
|
13
|
+
const directoryRead = 'app:instrument-center:directory:read';
|
|
14
|
+
const collegeRead = 'app:instrument-center:data:colleges:read';
|
|
15
|
+
const collegeCreate = 'app:instrument-center:data:colleges:create';
|
|
16
|
+
const collegeUpdate = 'app:instrument-center:data:colleges:update';
|
|
12
17
|
const allProtectedFieldCapabilities = Object.values(
|
|
13
18
|
protectedFieldCapabilities
|
|
14
19
|
).flatMap(capabilities => [capabilities.create, capabilities.update]);
|
|
@@ -26,6 +31,7 @@ export default defineOpenXiangdaApp({
|
|
|
26
31
|
root: 'apps/web',
|
|
27
32
|
routes: [
|
|
28
33
|
{ code: 'instruments', path: '/instruments', label: '仪器资源', surface: 'admin', navigation: 'menu', capability: read },
|
|
34
|
+
{ code: 'colleges', path: '/colleges', label: '学院字典', surface: 'admin', navigation: 'menu', capability: collegeRead },
|
|
29
35
|
{ code: 'instrument-create', path: '/instruments/new', label: '新增仪器', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: create },
|
|
30
36
|
{ code: 'instrument-edit', path: '/instruments/:id/edit', label: '编辑仪器', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: update },
|
|
31
37
|
{ code: 'instrument-detail', path: '/instruments/:id', label: '仪器详情', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: read },
|
|
@@ -54,11 +60,24 @@ export default defineOpenXiangdaApp({
|
|
|
54
60
|
{ code: inspectContext, kind: 'backend', name: '查看当前应用身份摘要' },
|
|
55
61
|
],
|
|
56
62
|
roles: [
|
|
57
|
-
{ code: 'school_admin', name: '学校管理员', capabilities: [read, create, update, remove, ...allProtectedFieldCapabilities, inspectContext] },
|
|
58
|
-
{ code: 'college_admin', name: '学院管理员', capabilities: [read, create, update, remove, ...collegeCreateCapabilities, ...collegeUpdateCapabilities, inspectContext] },
|
|
59
|
-
{ code: 'instrument_admin', name: '仪器管理员', capabilities: [read, update, inspectContext] },
|
|
63
|
+
{ code: 'school_admin', name: '学校管理员', capabilities: [read, create, update, remove, collegeRead, collegeCreate, collegeUpdate, directoryRead, ...allProtectedFieldCapabilities, inspectContext] },
|
|
64
|
+
{ code: 'college_admin', name: '学院管理员', capabilities: [read, create, update, remove, directoryRead, ...collegeCreateCapabilities, ...collegeUpdateCapabilities, inspectContext] },
|
|
65
|
+
{ code: 'instrument_admin', name: '仪器管理员', capabilities: [read, update, directoryRead, inspectContext] },
|
|
66
|
+
],
|
|
67
|
+
scopeDimensions: [
|
|
68
|
+
{
|
|
69
|
+
code: 'college',
|
|
70
|
+
name: '学院',
|
|
71
|
+
valueType: 'uuid',
|
|
72
|
+
hierarchyMode: 'flat',
|
|
73
|
+
valueSource: {
|
|
74
|
+
kind: 'native_resource',
|
|
75
|
+
resourceCode: 'colleges',
|
|
76
|
+
labelField: 'name',
|
|
77
|
+
enabledField: 'enabled',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
60
80
|
],
|
|
61
|
-
scopeDimensions: [{ code: 'college', name: '学院', valueType: 'string', hierarchyMode: 'flat' }],
|
|
62
81
|
dataPolicies: [
|
|
63
82
|
{
|
|
64
83
|
code: 'instrument-access',
|
|
@@ -73,5 +92,5 @@ export default defineOpenXiangdaApp({
|
|
|
73
92
|
},
|
|
74
93
|
],
|
|
75
94
|
},
|
|
76
|
-
data: { resources: [instruments] },
|
|
95
|
+
data: { resources: [colleges, instruments] },
|
|
77
96
|
});
|
package/template/package.json
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"build": "pnpm --recursive build"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"openxiangda-cli": "2.0.0-alpha.
|
|
25
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
24
|
+
"openxiangda-cli": "2.0.0-alpha.59",
|
|
25
|
+
"openxiangda-contracts": "2.0.0-alpha.28",
|
|
26
|
+
"openxiangda-devkit-core": "2.0.0-alpha.35",
|
|
27
27
|
"typescript": "5.9.3"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
export const compilerContractVersion = "native-2" as const;
|
|
3
3
|
export const appCode = "instrument-center" as const;
|
|
4
4
|
export const resourceCodes = [
|
|
5
|
+
"colleges",
|
|
5
6
|
"instruments"
|
|
6
7
|
] as const;
|
|
7
8
|
export const capabilities = [
|
|
9
|
+
"app:instrument-center:data:colleges:create",
|
|
10
|
+
"app:instrument-center:data:colleges:delete",
|
|
11
|
+
"app:instrument-center:data:colleges:read",
|
|
12
|
+
"app:instrument-center:data:colleges:update",
|
|
8
13
|
"app:instrument-center:data:instruments:create",
|
|
9
14
|
"app:instrument-center:data:instruments:delete",
|
|
10
15
|
"app:instrument-center:data:instruments:read",
|
|
@@ -39,6 +44,16 @@ export const appOperations = {
|
|
|
39
44
|
}
|
|
40
45
|
} as const;
|
|
41
46
|
export const appRoutes = {
|
|
47
|
+
colleges: {
|
|
48
|
+
"code": "colleges",
|
|
49
|
+
"path": "/colleges",
|
|
50
|
+
"label": "学院字典",
|
|
51
|
+
"surface": "admin",
|
|
52
|
+
"navigation": "menu",
|
|
53
|
+
"capability": "app:instrument-center:data:colleges:read",
|
|
54
|
+
"tabPersistence": "session",
|
|
55
|
+
"keepAlive": "none"
|
|
56
|
+
},
|
|
42
57
|
instrumentCreate: {
|
|
43
58
|
"code": "instrument-create",
|
|
44
59
|
"path": "/instruments/new",
|
|
@@ -95,6 +110,13 @@ export type EventType = (typeof eventTypes)[number];
|
|
|
95
110
|
export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
|
|
96
111
|
export type WorkflowCode = (typeof workflowCodes)[number];
|
|
97
112
|
|
|
113
|
+
export interface CollegesRecord {
|
|
114
|
+
id: string;
|
|
115
|
+
revision: number;
|
|
116
|
+
name: string;
|
|
117
|
+
enabled: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
export interface InstrumentsRecord {
|
|
99
121
|
id: string;
|
|
100
122
|
revision: number;
|