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
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
import {
|
|
5
|
-
addressControlValue,
|
|
6
|
-
addressDisplay,
|
|
7
|
-
addressStoredValue,
|
|
8
|
-
deepestAddressPredicate,
|
|
9
|
-
} from '../src/components/platform-fields/address-value';
|
|
10
|
-
|
|
11
|
-
const path = [
|
|
12
|
-
{ label: '浙江省', value: '330000' },
|
|
13
|
-
{ label: '杭州市', value: '330100' },
|
|
14
|
-
{ label: '西湖区', value: '330106' },
|
|
15
|
-
{ label: '翠苑街道', value: '330106012' },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
test('stores a complete administrative snapshot with a stable full address', () => {
|
|
19
|
-
const value = addressStoredValue(path, '文一路 1 号');
|
|
20
|
-
assert.deepEqual(value, {
|
|
21
|
-
country: { label: '中国', value: '100000' },
|
|
22
|
-
province: path[0],
|
|
23
|
-
city: path[1],
|
|
24
|
-
district: path[2],
|
|
25
|
-
street: path[3],
|
|
26
|
-
detail: '文一路 1 号',
|
|
27
|
-
fullAddress: '浙江省杭州市西湖区翠苑街道文一路 1 号',
|
|
28
|
-
});
|
|
29
|
-
assert.deepEqual(addressControlValue(value), path.map(item => item.value));
|
|
30
|
-
assert.equal(addressDisplay(value), '浙江省杭州市西湖区翠苑街道文一路 1 号');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('deduplicates adjacent municipality labels while retaining both snapshots', () => {
|
|
34
|
-
const value = addressStoredValue([
|
|
35
|
-
{ label: '上海市', value: '310000' },
|
|
36
|
-
{ label: '上海市', value: '310100' },
|
|
37
|
-
{ label: '浦东新区', value: '310115' },
|
|
38
|
-
]);
|
|
39
|
-
assert.equal(value?.fullAddress, '上海市浦东新区');
|
|
40
|
-
assert.equal(value?.province?.value, '310000');
|
|
41
|
-
assert.equal(value?.city?.value, '310100');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('uses the deepest selected stable code for Data API filtering', () => {
|
|
45
|
-
assert.deepEqual(
|
|
46
|
-
deepestAddressPredicate(addressStoredValue(path.slice(0, 3))),
|
|
47
|
-
{ path: 'district.value', value: '330106' }
|
|
48
|
-
);
|
|
49
|
-
assert.equal(addressStoredValue([], 'address only'), undefined);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('address field has lazy desktop and explicit mobile controls', () => {
|
|
53
|
-
const field = readFileSync(
|
|
54
|
-
new URL('../src/components/platform-fields/AddressField.tsx', import.meta.url),
|
|
55
|
-
'utf8'
|
|
56
|
-
);
|
|
57
|
-
const client = readFileSync(
|
|
58
|
-
new URL('../src/platform-client.ts', import.meta.url),
|
|
59
|
-
'utf8'
|
|
60
|
-
);
|
|
61
|
-
assert.match(field, /loadData=/);
|
|
62
|
-
assert.match(field, /loadChinaDivisions/);
|
|
63
|
-
assert.match(field, /placement="bottom"/);
|
|
64
|
-
assert.match(field, /ADDRESS_LEVELS\.map/);
|
|
65
|
-
assert.match(client, /\/china-divisions\//);
|
|
66
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import type { DataFieldOption } from 'openxiangda-contracts/browser';
|
|
4
|
-
import {
|
|
5
|
-
cascadeControlValue,
|
|
6
|
-
cascadeStoredValue,
|
|
7
|
-
cascadeTerminalValues,
|
|
8
|
-
} from '../src/components/platform-fields/cascade-value';
|
|
9
|
-
|
|
10
|
-
const options: DataFieldOption[] = [
|
|
11
|
-
{
|
|
12
|
-
value: 'equipment',
|
|
13
|
-
label: '设备',
|
|
14
|
-
children: [
|
|
15
|
-
{
|
|
16
|
-
value: 'optical',
|
|
17
|
-
label: '光学设备',
|
|
18
|
-
children: [{ value: 'microscope', label: '显微镜' }],
|
|
19
|
-
},
|
|
20
|
-
{ value: 'electrical', label: '电气设备' },
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
test('stores every labeled node for a cascade single path', () => {
|
|
26
|
-
const value = cascadeStoredValue(
|
|
27
|
-
options,
|
|
28
|
-
['equipment', 'optical', 'microscope'],
|
|
29
|
-
false
|
|
30
|
-
);
|
|
31
|
-
assert.deepEqual(value, [
|
|
32
|
-
{ value: 'equipment', label: '设备' },
|
|
33
|
-
{ value: 'optical', label: '光学设备' },
|
|
34
|
-
{ value: 'microscope', label: '显微镜' },
|
|
35
|
-
]);
|
|
36
|
-
assert.deepEqual(cascadeControlValue(value, false), [
|
|
37
|
-
'equipment',
|
|
38
|
-
'optical',
|
|
39
|
-
'microscope',
|
|
40
|
-
]);
|
|
41
|
-
assert.deepEqual(cascadeTerminalValues(value, false), ['microscope']);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('stores independent complete paths for cascade multiple', () => {
|
|
45
|
-
const value = cascadeStoredValue(
|
|
46
|
-
options,
|
|
47
|
-
[
|
|
48
|
-
['equipment', 'optical', 'microscope'],
|
|
49
|
-
['equipment', 'electrical'],
|
|
50
|
-
],
|
|
51
|
-
true
|
|
52
|
-
);
|
|
53
|
-
assert.deepEqual(cascadeTerminalValues(value, true), [
|
|
54
|
-
'microscope',
|
|
55
|
-
'electrical',
|
|
56
|
-
]);
|
|
57
|
-
assert.deepEqual(cascadeControlValue(value, true), [
|
|
58
|
-
['equipment', 'optical', 'microscope'],
|
|
59
|
-
['equipment', 'electrical'],
|
|
60
|
-
]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('drops forged paths that do not exist in declared options', () => {
|
|
64
|
-
assert.equal(
|
|
65
|
-
cascadeStoredValue(options, ['equipment', 'missing'], false),
|
|
66
|
-
undefined
|
|
67
|
-
);
|
|
68
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import type {
|
|
4
|
-
DepartmentReferenceValue,
|
|
5
|
-
DirectoryEntry,
|
|
6
|
-
UserReferenceValue,
|
|
7
|
-
} from 'openxiangda-contracts/browser';
|
|
8
|
-
import {
|
|
9
|
-
directoryEntryFromStored,
|
|
10
|
-
directoryStoredValueFromEntries,
|
|
11
|
-
} from '../src/components/platform-fields/directory-value';
|
|
12
|
-
|
|
13
|
-
test('preserves complete user snapshots through directory selection', () => {
|
|
14
|
-
const user: UserReferenceValue = {
|
|
15
|
-
value: 'user-1',
|
|
16
|
-
label: '张老师',
|
|
17
|
-
avatarUrl: 'https://cdn.example/avatar.png',
|
|
18
|
-
employeeNo: 'T001',
|
|
19
|
-
mobile: '13800000000',
|
|
20
|
-
email: 'teacher@example.test',
|
|
21
|
-
departments: [{
|
|
22
|
-
value: 'department-1',
|
|
23
|
-
label: '理学院',
|
|
24
|
-
fullPath: '学校 / 理学院',
|
|
25
|
-
}],
|
|
26
|
-
};
|
|
27
|
-
const entry = directoryEntryFromStored('user', user);
|
|
28
|
-
assert.equal(entry.description, 'T001 · 理学院');
|
|
29
|
-
assert.equal(directoryStoredValueFromEntries([entry], false), user);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('preserves department path and parent snapshots without resolving again', () => {
|
|
33
|
-
const department: DepartmentReferenceValue = {
|
|
34
|
-
value: 'department-2',
|
|
35
|
-
label: '实验中心',
|
|
36
|
-
fullPath: '学校 / 理学院 / 实验中心',
|
|
37
|
-
path: [
|
|
38
|
-
{ value: 'root', label: '学校' },
|
|
39
|
-
{ value: 'department-1', label: '理学院' },
|
|
40
|
-
{ value: 'department-2', label: '实验中心' },
|
|
41
|
-
],
|
|
42
|
-
parent: { value: 'department-1', label: '理学院' },
|
|
43
|
-
};
|
|
44
|
-
const entry = directoryEntryFromStored('department', department);
|
|
45
|
-
assert.equal(entry.parentId, 'department-1');
|
|
46
|
-
assert.deepEqual(entry.path?.map(item => item.id), [
|
|
47
|
-
'root',
|
|
48
|
-
'department-1',
|
|
49
|
-
'department-2',
|
|
50
|
-
]);
|
|
51
|
-
assert.deepEqual(
|
|
52
|
-
directoryStoredValueFromEntries([entry, entry] as DirectoryEntry[], true),
|
|
53
|
-
[department, department]
|
|
54
|
-
);
|
|
55
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
|
|
5
|
-
test('standard controls honor declared numeric, text, and file bounds', () => {
|
|
6
|
-
const source = readFileSync(
|
|
7
|
-
new URL('../src/components/resource/SurfaceFields.tsx', import.meta.url),
|
|
8
|
-
'utf8'
|
|
9
|
-
);
|
|
10
|
-
assert.match(source, /field\.type === 'number\.integer' \? 0 : field\.scale/);
|
|
11
|
-
assert.match(source, /10 \*\* -scale/);
|
|
12
|
-
assert.match(source, /field\.widget === 'money'/);
|
|
13
|
-
assert.match(source, /field\.widget === 'percent'/);
|
|
14
|
-
assert.match(source, /maxLength: field\.maxLength/);
|
|
15
|
-
assert.match(source, /file\.size > maxSizeMb \* 1024 \* 1024/);
|
|
16
|
-
assert.doesNotMatch(source, /<InputNumber min=\{0\}/);
|
|
17
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import type { DataFieldSurface, DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
-
import {
|
|
5
|
-
fieldValueForData,
|
|
6
|
-
fieldValueForForm,
|
|
7
|
-
} from '../src/components/platform-fields/field-form-codec';
|
|
8
|
-
|
|
9
|
-
function field(
|
|
10
|
-
type: DataFieldSurface['type'],
|
|
11
|
-
widget: DataFieldSurface['widget'],
|
|
12
|
-
input: Partial<DataFieldSurface> = {}
|
|
13
|
-
): DataFieldSurface {
|
|
14
|
-
return {
|
|
15
|
-
label: type,
|
|
16
|
-
type,
|
|
17
|
-
widget,
|
|
18
|
-
readCapabilities: [],
|
|
19
|
-
createCapabilities: [],
|
|
20
|
-
updateCapabilities: [],
|
|
21
|
-
...input,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('round-trips date, time and datetime values through form controls', () => {
|
|
26
|
-
const date = field('date', 'date');
|
|
27
|
-
const minute = field('time', 'time', { timePrecision: 'minute' });
|
|
28
|
-
const second = field('time', 'time', { timePrecision: 'second' });
|
|
29
|
-
const datetime = field('datetime', 'datetime');
|
|
30
|
-
assert.equal(fieldValueForData(date, fieldValueForForm(date, '2026-08-24')), '2026-08-24');
|
|
31
|
-
assert.equal(fieldValueForData(minute, fieldValueForForm(minute, '09:30:00')), '09:30');
|
|
32
|
-
assert.equal(fieldValueForData(second, fieldValueForForm(second, '09:30:45')), '09:30:45');
|
|
33
|
-
assert.equal(
|
|
34
|
-
fieldValueForData(datetime, fieldValueForForm(datetime, '2026-08-24T08:30:45.000Z')),
|
|
35
|
-
'2026-08-24T08:30:45.000Z'
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('round-trips date and datetime ranges as closed objects', () => {
|
|
40
|
-
const dateRange = field('date-range', 'date-range');
|
|
41
|
-
const datetimeRange = field('datetime-range', 'datetime-range');
|
|
42
|
-
assert.deepEqual(
|
|
43
|
-
fieldValueForData(
|
|
44
|
-
dateRange,
|
|
45
|
-
fieldValueForForm(dateRange, { start: '2026-08-01', end: '2026-08-31' })
|
|
46
|
-
),
|
|
47
|
-
{ start: '2026-08-01', end: '2026-08-31' }
|
|
48
|
-
);
|
|
49
|
-
assert.deepEqual(
|
|
50
|
-
fieldValueForData(
|
|
51
|
-
datetimeRange,
|
|
52
|
-
fieldValueForForm(datetimeRange, {
|
|
53
|
-
start: '2026-08-01T00:00:00.000Z',
|
|
54
|
-
end: '2026-08-31T00:00:00.000Z',
|
|
55
|
-
})
|
|
56
|
-
),
|
|
57
|
-
{
|
|
58
|
-
start: '2026-08-01T00:00:00.000Z',
|
|
59
|
-
end: '2026-08-31T00:00:00.000Z',
|
|
60
|
-
}
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('preserves explicit empty values for nullable fields', () => {
|
|
65
|
-
const surface = {
|
|
66
|
-
fields: { observedAt: field('datetime', 'datetime') },
|
|
67
|
-
} satisfies DataResourceSurface;
|
|
68
|
-
assert.equal(fieldValueForData(surface.fields.observedAt, null), null);
|
|
69
|
-
assert.equal(fieldValueForForm(surface.fields.observedAt, null), undefined);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('keeps query-facing date values free of Dayjs transport objects', () => {
|
|
73
|
-
const date = field('date', 'date');
|
|
74
|
-
const range = field('datetime-range', 'datetime-range');
|
|
75
|
-
const dateValue = fieldValueForData(date, fieldValueForForm(date, '2026-08-24'));
|
|
76
|
-
const rangeValue = fieldValueForData(
|
|
77
|
-
range,
|
|
78
|
-
fieldValueForForm(range, {
|
|
79
|
-
start: '2026-08-01T00:00:00.000Z',
|
|
80
|
-
end: '2026-08-31T00:00:00.000Z',
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
assert.equal(typeof dateValue, 'string');
|
|
84
|
-
assert.deepEqual(JSON.parse(JSON.stringify(rangeValue)), rangeValue);
|
|
85
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
import {
|
|
5
|
-
browserLocationValue,
|
|
6
|
-
dingTalkLocationValue,
|
|
7
|
-
} from '../src/components/platform-fields/location-value';
|
|
8
|
-
|
|
9
|
-
test('normalizes browser coordinates into an exact snapshot', () => {
|
|
10
|
-
assert.deepEqual(
|
|
11
|
-
browserLocationValue({
|
|
12
|
-
longitude: 120.1234567,
|
|
13
|
-
latitude: 30.7654321,
|
|
14
|
-
accuracy: 8.5,
|
|
15
|
-
timestamp: Date.parse('2026-08-24T08:00:00.000Z'),
|
|
16
|
-
}),
|
|
17
|
-
{
|
|
18
|
-
source: 'browser',
|
|
19
|
-
longitude: 120.1234567,
|
|
20
|
-
latitude: 30.7654321,
|
|
21
|
-
accuracy: 8.5,
|
|
22
|
-
capturedAt: '2026-08-24T08:00:00.000Z',
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('normalizes DingTalk coordinates and optional reverse-geocoded display data', () => {
|
|
28
|
-
assert.deepEqual(
|
|
29
|
-
dingTalkLocationValue(
|
|
30
|
-
{
|
|
31
|
-
longitude: '120.1234567',
|
|
32
|
-
latitude: '30.7654321',
|
|
33
|
-
accuracy: '12',
|
|
34
|
-
address: ' 浙江省杭州市西湖区 ',
|
|
35
|
-
poiName: '测试大楼',
|
|
36
|
-
province: '浙江省',
|
|
37
|
-
city: '杭州市',
|
|
38
|
-
district: '西湖区',
|
|
39
|
-
},
|
|
40
|
-
Date.parse('2026-08-24T08:00:00.000Z')
|
|
41
|
-
),
|
|
42
|
-
{
|
|
43
|
-
source: 'dingTalk',
|
|
44
|
-
longitude: 120.1234567,
|
|
45
|
-
latitude: 30.7654321,
|
|
46
|
-
accuracy: 12,
|
|
47
|
-
address: '浙江省杭州市西湖区',
|
|
48
|
-
name: '测试大楼',
|
|
49
|
-
province: '浙江省',
|
|
50
|
-
city: '杭州市',
|
|
51
|
-
district: '西湖区',
|
|
52
|
-
capturedAt: '2026-08-24T08:00:00.000Z',
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('rejects missing and out-of-range coordinates', () => {
|
|
58
|
-
assert.throws(
|
|
59
|
-
() => dingTalkLocationValue({ latitude: 30 }),
|
|
60
|
-
/有效经度/
|
|
61
|
-
);
|
|
62
|
-
assert.throws(
|
|
63
|
-
() => browserLocationValue({ longitude: 181, latitude: 30 }),
|
|
64
|
-
/有效经度/
|
|
65
|
-
);
|
|
66
|
-
assert.throws(
|
|
67
|
-
() => browserLocationValue({ longitude: 120, latitude: -91 }),
|
|
68
|
-
/有效纬度/
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('location control captures coordinates without a manual address input', () => {
|
|
73
|
-
const source = readFileSync(
|
|
74
|
-
new URL('../src/components/platform-fields/LocationField.tsx', import.meta.url),
|
|
75
|
-
'utf8'
|
|
76
|
-
);
|
|
77
|
-
assert.match(source, /coordinate: 0/);
|
|
78
|
-
assert.match(source, /withReGeocode: true/);
|
|
79
|
-
assert.match(source, /browserLocationValue/);
|
|
80
|
-
assert.match(source, /dingTalkLocationValue/);
|
|
81
|
-
assert.match(source, /LocationValueDisplay/);
|
|
82
|
-
assert.doesNotMatch(source, /<Input|<textarea|contentEditable/);
|
|
83
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import type { DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
-
import { parseResourceImportMatrix } from '../src/components/resource/resource-import';
|
|
5
|
-
|
|
6
|
-
const surface = {
|
|
7
|
-
generated: true,
|
|
8
|
-
fields: {
|
|
9
|
-
visitorName: { label: '访客姓名', type: 'text.short', widget: 'text', requiredHint: true },
|
|
10
|
-
visitorCount: { label: '来访人数', type: 'number.integer', widget: 'number' },
|
|
11
|
-
approved: { label: '是否通过', type: 'boolean', widget: 'switch' },
|
|
12
|
-
status: {
|
|
13
|
-
label: '状态',
|
|
14
|
-
type: 'option.single',
|
|
15
|
-
widget: 'select',
|
|
16
|
-
options: [
|
|
17
|
-
{ label: '待审核', value: 'pending' },
|
|
18
|
-
{ label: '已通过', value: 'approved' },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
visitDate: { label: '来访日期', type: 'date', widget: 'date' },
|
|
22
|
-
attachments: { label: '附件', type: 'file', widget: 'attachment' },
|
|
23
|
-
},
|
|
24
|
-
} as unknown as DataResourceSurface;
|
|
25
|
-
|
|
26
|
-
test('maps exact labels and codes into one canonical create transaction', () => {
|
|
27
|
-
const preview = parseResourceImportMatrix(
|
|
28
|
-
'访客.xlsx',
|
|
29
|
-
[
|
|
30
|
-
['访客姓名', 'visitorCount', '是否通过', '状态'],
|
|
31
|
-
['张三', 2, '是', '待审核'],
|
|
32
|
-
['李四', '3', '否', 'approved'],
|
|
33
|
-
],
|
|
34
|
-
'visitor-reservations',
|
|
35
|
-
surface,
|
|
36
|
-
['visitorName', 'visitorCount', 'approved', 'status']
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
assert.deepEqual(preview.errors, []);
|
|
40
|
-
assert.deepEqual(preview.operations, [
|
|
41
|
-
{
|
|
42
|
-
operation: 'create',
|
|
43
|
-
resourceCode: 'visitor-reservations',
|
|
44
|
-
data: {
|
|
45
|
-
visitorName: '张三',
|
|
46
|
-
visitorCount: 2,
|
|
47
|
-
approved: true,
|
|
48
|
-
status: { label: '待审核', value: 'pending' },
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
operation: 'create',
|
|
53
|
-
resourceCode: 'visitor-reservations',
|
|
54
|
-
data: {
|
|
55
|
-
visitorName: '李四',
|
|
56
|
-
visitorCount: 3,
|
|
57
|
-
approved: false,
|
|
58
|
-
status: { label: '已通过', value: 'approved' },
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('preserves exact dates and rejects impossible calendar dates', () => {
|
|
65
|
-
const valid = parseResourceImportMatrix(
|
|
66
|
-
'日期.csv',
|
|
67
|
-
[
|
|
68
|
-
['访客姓名', '来访日期'],
|
|
69
|
-
['张三', '2026-08-23'],
|
|
70
|
-
],
|
|
71
|
-
'visitor-reservations',
|
|
72
|
-
surface,
|
|
73
|
-
['visitorName', 'visitDate']
|
|
74
|
-
);
|
|
75
|
-
assert.deepEqual(valid.errors, []);
|
|
76
|
-
assert.deepEqual(valid.operations[0], {
|
|
77
|
-
operation: 'create',
|
|
78
|
-
resourceCode: 'visitor-reservations',
|
|
79
|
-
data: { visitorName: '张三', visitDate: '2026-08-23' },
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const invalid = parseResourceImportMatrix(
|
|
83
|
-
'错误日期.csv',
|
|
84
|
-
[
|
|
85
|
-
['访客姓名', '来访日期'],
|
|
86
|
-
['张三', '2026-02-30'],
|
|
87
|
-
],
|
|
88
|
-
'visitor-reservations',
|
|
89
|
-
surface,
|
|
90
|
-
['visitorName', 'visitDate']
|
|
91
|
-
);
|
|
92
|
-
assert.equal(invalid.operations.length, 0);
|
|
93
|
-
assert.ok(invalid.errors.some((error) => error.includes('日期格式无效')));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('rejects values in a column without a header', () => {
|
|
97
|
-
const preview = parseResourceImportMatrix(
|
|
98
|
-
'空表头.csv',
|
|
99
|
-
[
|
|
100
|
-
['访客姓名', ''],
|
|
101
|
-
['张三', '未声明的数据'],
|
|
102
|
-
],
|
|
103
|
-
'visitor-reservations',
|
|
104
|
-
surface,
|
|
105
|
-
['visitorName']
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
assert.equal(preview.operations.length, 0);
|
|
109
|
-
assert.ok(preview.errors.some((error) => error.includes('缺少表头')));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('rejects unknown, managed-file and invalid cells before submission', () => {
|
|
113
|
-
const preview = parseResourceImportMatrix(
|
|
114
|
-
'错误.csv',
|
|
115
|
-
[
|
|
116
|
-
['访客姓名', '是否通过', '附件', '任意列'],
|
|
117
|
-
['', '不确定', 'https://untrusted.example/file', 'value'],
|
|
118
|
-
],
|
|
119
|
-
'visitor-reservations',
|
|
120
|
-
surface,
|
|
121
|
-
['visitorName', 'approved', 'attachments']
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
assert.equal(preview.operations.length, 0);
|
|
125
|
-
assert.ok(preview.errors.some((error) => error.includes('不是已声明字段')));
|
|
126
|
-
assert.ok(preview.errors.some((error) => error.includes('必须通过平台上传组件')));
|
|
127
|
-
assert.ok(preview.errors.some((error) => error.includes('请填写是或否')));
|
|
128
|
-
assert.ok(preview.errors.some((error) => error.includes('访客姓名为必填项')));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('keeps the atomic import boundary at one hundred rows', () => {
|
|
132
|
-
const preview = parseResourceImportMatrix(
|
|
133
|
-
'过大.csv',
|
|
134
|
-
[['访客姓名'], ...Array.from({ length: 101 }, (_, index) => [`访客 ${index + 1}`])],
|
|
135
|
-
'visitor-reservations',
|
|
136
|
-
surface,
|
|
137
|
-
['visitorName']
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
assert.equal(preview.rows.length, 100);
|
|
141
|
-
assert.equal(preview.operations.length, 0);
|
|
142
|
-
assert.ok(preview.errors.some((error) => error.includes('单次最多导入 100 行')));
|
|
143
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import type { DataFieldSurface, DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
-
import { buildResourceWhere } from '../src/components/platform-fields/resource-query';
|
|
5
|
-
|
|
6
|
-
function field(
|
|
7
|
-
type: DataFieldSurface['type'],
|
|
8
|
-
widget: DataFieldSurface['widget']
|
|
9
|
-
): DataFieldSurface {
|
|
10
|
-
return {
|
|
11
|
-
label: type,
|
|
12
|
-
type,
|
|
13
|
-
widget,
|
|
14
|
-
readCapabilities: [],
|
|
15
|
-
createCapabilities: [],
|
|
16
|
-
updateCapabilities: [],
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const surface = {
|
|
21
|
-
fields: {
|
|
22
|
-
name: field('text.short', 'text'),
|
|
23
|
-
status: field('option.single', 'select'),
|
|
24
|
-
tags: field('option.multiple', 'multi-select'),
|
|
25
|
-
category: field('cascade.single', 'cascade'),
|
|
26
|
-
categories: field('cascade.multiple', 'cascade'),
|
|
27
|
-
address: field('address', 'address'),
|
|
28
|
-
metadata: field('json', 'json'),
|
|
29
|
-
period: field('date-range', 'date-range'),
|
|
30
|
-
},
|
|
31
|
-
list: { searchableFields: ['name'] },
|
|
32
|
-
} satisfies DataResourceSurface;
|
|
33
|
-
|
|
34
|
-
test('builds canonical snapshot and text predicates', () => {
|
|
35
|
-
assert.deepEqual(
|
|
36
|
-
buildResourceWhere('records', surface, {
|
|
37
|
-
page: 1,
|
|
38
|
-
pageSize: 20,
|
|
39
|
-
keyword: ' 显微镜 ',
|
|
40
|
-
filters: {
|
|
41
|
-
status: { value: 'enabled', label: '启用' },
|
|
42
|
-
tags: [
|
|
43
|
-
{ value: 'precision', label: '精密' },
|
|
44
|
-
{ value: 'shared', label: '共享' },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
}),
|
|
48
|
-
{
|
|
49
|
-
and: [
|
|
50
|
-
{ field: 'name', operator: 'contains', value: '显微镜' },
|
|
51
|
-
{ field: 'status', operator: 'eq', value: 'enabled' },
|
|
52
|
-
{ field: 'tags', operator: 'hasAny', value: ['precision', 'shared'] },
|
|
53
|
-
],
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('builds cascade membership and semantic range predicates', () => {
|
|
59
|
-
assert.deepEqual(
|
|
60
|
-
buildResourceWhere('records', surface, {
|
|
61
|
-
page: 1,
|
|
62
|
-
pageSize: 20,
|
|
63
|
-
filters: {
|
|
64
|
-
category: [
|
|
65
|
-
{ value: 'equipment', label: '设备' },
|
|
66
|
-
{ value: 'microscope', label: '显微镜' },
|
|
67
|
-
],
|
|
68
|
-
categories: [
|
|
69
|
-
[
|
|
70
|
-
{ value: 'equipment', label: '设备' },
|
|
71
|
-
{ value: 'microscope', label: '显微镜' },
|
|
72
|
-
],
|
|
73
|
-
[{ value: 'rooms', label: '场地' }],
|
|
74
|
-
],
|
|
75
|
-
period: { start: '2026-08-01', end: '2026-08-31' },
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
{
|
|
79
|
-
and: [
|
|
80
|
-
{ field: 'category', operator: 'has', value: 'microscope' },
|
|
81
|
-
{ field: 'categories', operator: 'hasAny', value: ['microscope', 'rooms'] },
|
|
82
|
-
{
|
|
83
|
-
field: 'period',
|
|
84
|
-
operator: 'overlaps',
|
|
85
|
-
value: { start: '2026-08-01', end: '2026-08-31' },
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('ignores stale-route and empty filters without emitting legacy operators', () => {
|
|
93
|
-
assert.equal(
|
|
94
|
-
buildResourceWhere('records', surface, {
|
|
95
|
-
page: 1,
|
|
96
|
-
pageSize: 20,
|
|
97
|
-
filters: { stale: 'value', tags: [] },
|
|
98
|
-
}),
|
|
99
|
-
undefined
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('filters an address by its deepest stable administrative code', () => {
|
|
104
|
-
assert.deepEqual(
|
|
105
|
-
buildResourceWhere('records', surface, {
|
|
106
|
-
page: 1,
|
|
107
|
-
pageSize: 20,
|
|
108
|
-
filters: {
|
|
109
|
-
address: {
|
|
110
|
-
country: { label: '中国', value: '100000' },
|
|
111
|
-
province: { label: '浙江省', value: '330000' },
|
|
112
|
-
city: { label: '杭州市', value: '330100' },
|
|
113
|
-
district: { label: '西湖区', value: '330106' },
|
|
114
|
-
fullAddress: '浙江省杭州市西湖区',
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
{
|
|
119
|
-
and: [{
|
|
120
|
-
field: 'address',
|
|
121
|
-
operator: 'jsonContains',
|
|
122
|
-
path: 'district.value',
|
|
123
|
-
value: '330106',
|
|
124
|
-
}],
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('filters bounded JSON with canonical containment', () => {
|
|
130
|
-
assert.deepEqual(
|
|
131
|
-
buildResourceWhere('records', surface, {
|
|
132
|
-
page: 1,
|
|
133
|
-
pageSize: 20,
|
|
134
|
-
filters: { metadata: { enabled: true, level: 2 } },
|
|
135
|
-
}),
|
|
136
|
-
{
|
|
137
|
-
and: [{
|
|
138
|
-
field: 'metadata',
|
|
139
|
-
operator: 'jsonContains',
|
|
140
|
-
value: { enabled: true, level: 2 },
|
|
141
|
-
}],
|
|
142
|
-
}
|
|
143
|
-
);
|
|
144
|
-
});
|