openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.86
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/package.json +4 -4
- package/template/AGENTS.md +4 -4
- package/template/README.md +2 -2
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
- package/template/apps/web/e2e/resources.spec.ts +258 -20
- package/template/apps/web/field-protocol.e2e.html +12 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +3 -1
- package/template/apps/web/scripts/check.mjs +8 -5
- package/template/apps/web/scripts/verify-build.mjs +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
- package/template/apps/web/src/FilePreviewPage.tsx +30 -9
- package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
- package/template/apps/web/src/Shell.tsx +93 -6
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
- package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
- package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
- package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
- package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
- package/template/apps/web/src/components/resource/resource-import.ts +4 -4
- package/template/apps/web/src/data-provider.ts +59 -34
- package/template/apps/web/src/platform-client.ts +341 -136
- package/template/apps/web/src/runtime.tsx +156 -17
- package/template/apps/web/src/styles.css +264 -0
- package/template/apps/web/test/address-field.test.ts +66 -0
- package/template/apps/web/test/cascade-value.test.ts +68 -0
- package/template/apps/web/test/contracts.test.ts +29 -5
- package/template/apps/web/test/directory-value.test.ts +55 -0
- package/template/apps/web/test/field-bounds.test.ts +17 -0
- package/template/apps/web/test/field-form-codec.test.ts +85 -0
- package/template/apps/web/test/location-field.test.ts +83 -0
- package/template/apps/web/test/resource-import.test.ts +8 -7
- package/template/apps/web/test/resource-query.test.ts +144 -0
- package/template/apps/web/test/rich-json-field.test.ts +53 -0
- package/template/apps/web/test/signature-serial-field.test.ts +40 -0
- package/template/apps/web/test/subtable-value.test.ts +144 -0
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +69 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import type { DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
+
import {
|
|
5
|
+
buildSubtableOperations,
|
|
6
|
+
type SubtableDraftRow,
|
|
7
|
+
} from '../src/components/platform-fields/subtable-value';
|
|
8
|
+
|
|
9
|
+
const childSurface: DataResourceSurface = {
|
|
10
|
+
fields: {
|
|
11
|
+
parent_id: field('uuid'),
|
|
12
|
+
display_order: field('number.integer'),
|
|
13
|
+
description: field('text.short'),
|
|
14
|
+
private_note: {
|
|
15
|
+
...field('text.long'),
|
|
16
|
+
createCapabilities: [],
|
|
17
|
+
updateCapabilities: [],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('plans parent references, updates, deletes and deterministic reorder', () => {
|
|
23
|
+
const rows: SubtableDraftRow[] = [
|
|
24
|
+
{
|
|
25
|
+
key: 'new',
|
|
26
|
+
state: 'created',
|
|
27
|
+
data: { description: 'New', private_note: 'forbidden' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'existing',
|
|
31
|
+
state: 'persisted',
|
|
32
|
+
id: 'child-1',
|
|
33
|
+
revision: 3,
|
|
34
|
+
originalOrder: 0,
|
|
35
|
+
originalData: { description: 'Old' },
|
|
36
|
+
data: { description: 'Changed' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'deleted',
|
|
40
|
+
state: 'deleted',
|
|
41
|
+
id: 'child-2',
|
|
42
|
+
revision: 2,
|
|
43
|
+
data: {},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const operations = buildSubtableOperations({
|
|
47
|
+
rows,
|
|
48
|
+
childResourceCode: 'children',
|
|
49
|
+
childSurface,
|
|
50
|
+
foreignKey: 'parent_id',
|
|
51
|
+
orderField: 'display_order',
|
|
52
|
+
parent: { operation: 'create', operationIndex: 0 },
|
|
53
|
+
canWrite: (_code, field, operation) =>
|
|
54
|
+
(operation === 'create'
|
|
55
|
+
? field.createCapabilities
|
|
56
|
+
: field.updateCapabilities
|
|
57
|
+
).length > 0,
|
|
58
|
+
canDelete: true,
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(operations, [
|
|
61
|
+
{
|
|
62
|
+
operation: 'delete',
|
|
63
|
+
resourceCode: 'children',
|
|
64
|
+
id: 'child-2',
|
|
65
|
+
expectedRevision: 2,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
operation: 'create',
|
|
69
|
+
resourceCode: 'children',
|
|
70
|
+
data: {
|
|
71
|
+
description: 'New',
|
|
72
|
+
parent_id: { operationIndex: 0, field: 'id' },
|
|
73
|
+
display_order: 0,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
operation: 'update',
|
|
78
|
+
resourceCode: 'children',
|
|
79
|
+
id: 'child-1',
|
|
80
|
+
expectedRevision: 3,
|
|
81
|
+
data: { description: 'Changed', display_order: 1 },
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('omits unchanged persisted rows and enforces the transaction-derived row bound', () => {
|
|
87
|
+
const unchanged: SubtableDraftRow = {
|
|
88
|
+
key: 'existing',
|
|
89
|
+
state: 'persisted',
|
|
90
|
+
id: 'child-1',
|
|
91
|
+
revision: 1,
|
|
92
|
+
originalOrder: 0,
|
|
93
|
+
originalData: { description: 'Same' },
|
|
94
|
+
data: { description: 'Same' },
|
|
95
|
+
};
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
buildSubtableOperations({
|
|
98
|
+
rows: [unchanged],
|
|
99
|
+
childResourceCode: 'children',
|
|
100
|
+
childSurface,
|
|
101
|
+
foreignKey: 'parent_id',
|
|
102
|
+
orderField: 'display_order',
|
|
103
|
+
parent: { operation: 'update', id: 'parent-1' },
|
|
104
|
+
canWrite: () => true,
|
|
105
|
+
canDelete: true,
|
|
106
|
+
}),
|
|
107
|
+
[]
|
|
108
|
+
);
|
|
109
|
+
assert.throws(
|
|
110
|
+
() =>
|
|
111
|
+
buildSubtableOperations({
|
|
112
|
+
rows: Array.from({ length: 50 }, (_, index) => ({
|
|
113
|
+
key: String(index),
|
|
114
|
+
state: 'created' as const,
|
|
115
|
+
data: { description: String(index) },
|
|
116
|
+
})),
|
|
117
|
+
childResourceCode: 'children',
|
|
118
|
+
childSurface,
|
|
119
|
+
foreignKey: 'parent_id',
|
|
120
|
+
orderField: 'display_order',
|
|
121
|
+
maxRows: 49,
|
|
122
|
+
parent: { operation: 'update', id: 'parent-1' },
|
|
123
|
+
canWrite: () => true,
|
|
124
|
+
canDelete: true,
|
|
125
|
+
}),
|
|
126
|
+
/OPENXIANGDA_SUBTABLE_MAX_ROWS_EXCEEDED/
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function field(type: string) {
|
|
131
|
+
return {
|
|
132
|
+
label: type,
|
|
133
|
+
type,
|
|
134
|
+
widget:
|
|
135
|
+
type === 'uuid'
|
|
136
|
+
? 'readonly'
|
|
137
|
+
: type === 'number.integer'
|
|
138
|
+
? 'number'
|
|
139
|
+
: 'text',
|
|
140
|
+
readCapabilities: ['read'],
|
|
141
|
+
createCapabilities: ['create'],
|
|
142
|
+
updateCapabilities: ['update'],
|
|
143
|
+
} as any;
|
|
144
|
+
}
|
package/template/package.json
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"build": "pnpm --recursive build"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"openxiangda-cli": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
25
|
+
"openxiangda-cli": "2.0.0-alpha.86",
|
|
26
|
+
"openxiangda-contracts": "2.0.0-alpha.40",
|
|
27
|
+
"openxiangda-devkit-core": "2.0.0-alpha.53",
|
|
28
28
|
"typescript": "5.9.3"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -26,3 +26,72 @@ export type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
|
|
|
26
26
|
export type EventType = (typeof eventTypes)[number];
|
|
27
27
|
export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
|
|
28
28
|
export type WorkflowCode = (typeof workflowCodes)[number];
|
|
29
|
+
|
|
30
|
+
export interface LabeledValue {
|
|
31
|
+
label: string;
|
|
32
|
+
value: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
color?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ResourceReferenceValue extends LabeledValue {
|
|
37
|
+
resourceCode: string;
|
|
38
|
+
snapshot?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
export interface DepartmentReferenceValue extends LabeledValue {
|
|
41
|
+
fullPath?: string;
|
|
42
|
+
path?: LabeledValue[];
|
|
43
|
+
parent?: LabeledValue;
|
|
44
|
+
}
|
|
45
|
+
export interface UserReferenceValue extends LabeledValue {
|
|
46
|
+
avatarUrl?: string;
|
|
47
|
+
employeeNo?: string;
|
|
48
|
+
title?: string;
|
|
49
|
+
mobile?: string;
|
|
50
|
+
email?: string;
|
|
51
|
+
departments?: DepartmentReferenceValue[];
|
|
52
|
+
}
|
|
53
|
+
export interface DateRangeValue { start: string; end: string; }
|
|
54
|
+
export type CascadePathValue = LabeledValue[];
|
|
55
|
+
export interface DataFileRef {
|
|
56
|
+
schemaVersion: 'openxiangda.data-file-ref/v2';
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
size: number;
|
|
60
|
+
contentType: string;
|
|
61
|
+
}
|
|
62
|
+
export interface DataImageRef extends DataFileRef {
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
thumbnailUrl: string;
|
|
66
|
+
previewUrl: string;
|
|
67
|
+
}
|
|
68
|
+
export interface StableSignatureValue {
|
|
69
|
+
file: Omit<DataFileRef, 'schemaVersion'>;
|
|
70
|
+
signer?: UserReferenceValue;
|
|
71
|
+
signedAt: string;
|
|
72
|
+
points?: Array<{ x: number; y: number; t: number }>;
|
|
73
|
+
hash: string;
|
|
74
|
+
}
|
|
75
|
+
export interface StableAddressValue {
|
|
76
|
+
country?: LabeledValue;
|
|
77
|
+
province?: LabeledValue;
|
|
78
|
+
city?: LabeledValue;
|
|
79
|
+
district?: LabeledValue;
|
|
80
|
+
street?: LabeledValue;
|
|
81
|
+
detail?: string;
|
|
82
|
+
fullAddress?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface StableLocationSnapshot {
|
|
85
|
+
address?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
province?: string;
|
|
88
|
+
city?: string;
|
|
89
|
+
district?: string;
|
|
90
|
+
accuracy?: number;
|
|
91
|
+
capturedAt?: string;
|
|
92
|
+
}
|
|
93
|
+
export type StableLocationValue = StableLocationSnapshot & {
|
|
94
|
+
source: 'browser' | 'dingTalk';
|
|
95
|
+
longitude: number;
|
|
96
|
+
latitude: number;
|
|
97
|
+
};
|
|
@@ -9,14 +9,18 @@ function visit(directory) {
|
|
|
9
9
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
10
10
|
const path = join(directory, entry.name);
|
|
11
11
|
const name = relative(root, path).replaceAll('\\', '/');
|
|
12
|
-
if (
|
|
12
|
+
if (
|
|
13
|
+
generated.test(name) ||
|
|
14
|
+
name === 'pnpm-lock.yaml' ||
|
|
15
|
+
name === 'packages/contracts/src/generated.ts'
|
|
16
|
+
) continue;
|
|
13
17
|
if (entry.isDirectory()) visit(path);
|
|
14
18
|
else if (entry.isFile() && sourceExtensions.test(name)) files.push(path);
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
visit(root);
|
|
18
22
|
const bytes = files.reduce((total, file) => total + statSync(file).size, 0);
|
|
19
|
-
if (files.length >
|
|
23
|
+
if (files.length > 80) throw new Error(`TEMPLATE_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>80`);
|
|
20
24
|
if (bytes > 500_000) throw new Error(`TEMPLATE_SOURCE_BYTE_BUDGET_EXCEEDED:${bytes}>500000`);
|
|
21
25
|
const productionFiles = files.filter(file => {
|
|
22
26
|
const name = relative(root, file).replaceAll('\\', '/');
|
|
@@ -33,8 +37,11 @@ const productionFiles = files.filter(file => {
|
|
|
33
37
|
const source = productionFiles.map(file => readFileSync(file, 'utf8')).join('\n');
|
|
34
38
|
const agents = readFileSync(resolve(root, 'AGENTS.md'), 'utf8');
|
|
35
39
|
if (!agents.includes('pnpm openxiangda')) throw new Error('TEMPLATE_AGENT_PINNED_CLI_MISSING');
|
|
36
|
-
if (
|
|
37
|
-
|
|
40
|
+
if (
|
|
41
|
+
!agents.includes("union of that user's application roles") ||
|
|
42
|
+
!agents.includes('opaque authorization context')
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('TEMPLATE_AGENT_ROLE_UNION_BOUNDARY_MISSING');
|
|
38
45
|
}
|
|
39
46
|
for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds', 'college-' + 'am', 'user-' + 'wang', 'dept-' + 'am-test']) {
|
|
40
47
|
if (source.includes(marker)) throw new Error(`TEMPLATE_FORBIDDEN_MARKER:${marker}`);
|