openxiangda-cli 2.0.0-alpha.86 → 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.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,326 +0,0 @@
1
- import { DeleteOutlined, EditOutlined, ImportOutlined } from '@ant-design/icons';
2
- import { Alert, App, Button, Form, Modal, Select, Space, Table, Typography, Upload } from 'antd';
3
- import dayjs from 'dayjs';
4
- import type { DataResourceSurface } from 'openxiangda-contracts/browser';
5
- import { useMemo, useState } from 'react';
6
- import { transactNativeData } from '../../platform-client';
7
- import { SurfaceFieldControl, SurfaceFieldValue, type SurfaceField } from './SurfaceFields';
8
- import { parseResourceImportFile, type ResourceImportPreview } from './resource-import';
9
-
10
- type ResourceRecord = Record<string, unknown> & {
11
- id: string;
12
- revision: number;
13
- };
14
-
15
- export function ResourceImportButton({
16
- code,
17
- name,
18
- surface,
19
- writableFieldCodes,
20
- onCompleted,
21
- }: {
22
- code: string;
23
- name: string;
24
- surface: DataResourceSurface;
25
- writableFieldCodes: string[];
26
- onCompleted: () => Promise<unknown> | unknown;
27
- }) {
28
- const { message } = App.useApp();
29
- const [preview, setPreview] = useState<ResourceImportPreview>();
30
- const [parsing, setParsing] = useState(false);
31
- const [submitting, setSubmitting] = useState(false);
32
- const [idempotencyKey, setIdempotencyKey] = useState('');
33
- const close = () => {
34
- if (submitting) return;
35
- setPreview(undefined);
36
- setIdempotencyKey('');
37
- };
38
- const submit = async () => {
39
- if (!preview?.operations.length || preview.errors.length) return;
40
- setSubmitting(true);
41
- try {
42
- const key = idempotencyKey || crypto.randomUUID();
43
- if (!idempotencyKey) setIdempotencyKey(key);
44
- const result = await transactNativeData(preview.operations, key);
45
- message.success(`已导入 ${result.items.length} 条${name}`);
46
- await onCompleted();
47
- setPreview(undefined);
48
- setIdempotencyKey('');
49
- } catch (error) {
50
- message.error(transactionFailureMessage(error, '导入失败'));
51
- } finally {
52
- setSubmitting(false);
53
- }
54
- };
55
- const previewColumns = useMemo(
56
- () => [
57
- { title: '行号', dataIndex: 'rowNumber', width: 72 },
58
- ...Object.entries(surface.fields)
59
- .filter(([code]) => writableFieldCodes.includes(code))
60
- .map(([fieldCode, field]) => ({
61
- title: field.label,
62
- key: fieldCode,
63
- render: (_: unknown, row: ResourceImportPreview['rows'][number]) => (
64
- <SurfaceFieldValue field={{ key: fieldCode, ...field }} value={row.data[fieldCode]} />
65
- ),
66
- })),
67
- ],
68
- [surface.fields, writableFieldCodes]
69
- );
70
- return (
71
- <>
72
- <Upload
73
- accept=".csv,.xls,.xlsx"
74
- beforeUpload={async (file) => {
75
- setParsing(true);
76
- try {
77
- setPreview(await parseResourceImportFile(file, code, surface, writableFieldCodes));
78
- setIdempotencyKey(crypto.randomUUID());
79
- } catch (error) {
80
- message.error(error instanceof Error ? error.message : '文件解析失败');
81
- } finally {
82
- setParsing(false);
83
- }
84
- return Upload.LIST_IGNORE;
85
- }}
86
- maxCount={1}
87
- showUploadList={false}
88
- >
89
- <Button
90
- disabled={!writableFieldCodes.length}
91
- icon={<ImportOutlined />}
92
- loading={parsing}
93
- title={!writableFieldCodes.length ? '当前角色没有可导入字段' : undefined}
94
- >
95
- 导入
96
- </Button>
97
- </Upload>
98
- <Modal
99
- cancelText="取消"
100
- destroyOnHidden
101
- okButtonProps={{ disabled: Boolean(preview?.errors.length) }}
102
- okText={`确认导入${preview?.operations.length ? ` ${preview.operations.length} 条` : ''}`}
103
- open={Boolean(preview)}
104
- title={`导入${name}`}
105
- width={960}
106
- confirmLoading={submitting}
107
- onCancel={close}
108
- onOk={() => void submit()}
109
- >
110
- {preview && (
111
- <Space direction="vertical" size="middle" style={{ width: '100%' }}>
112
- <Typography.Text type="secondary">{preview.fileName} · 单次最多 100 行,所有数据将作为一个事务提交</Typography.Text>
113
- {preview.errors.length > 0 && (
114
- <Alert
115
- description={
116
- <ul className="oxa-import-errors">
117
- {preview.errors.slice(0, 20).map((error) => (
118
- <li key={error}>{error}</li>
119
- ))}
120
- {preview.errors.length > 20 && <li>另有 {preview.errors.length - 20} 个错误</li>}
121
- </ul>
122
- }
123
- message="请先修正导入文件"
124
- showIcon
125
- type="error"
126
- />
127
- )}
128
- <Table
129
- columns={previewColumns}
130
- dataSource={preview.rows.slice(0, 20)}
131
- locale={{ emptyText: '没有可预览的数据' }}
132
- pagination={false}
133
- rowKey="rowNumber"
134
- scroll={{ x: 'max-content', y: 360 }}
135
- size="small"
136
- />
137
- {preview.rows.length > 20 && <Typography.Text type="secondary">仅预览前 20 行</Typography.Text>}
138
- </Space>
139
- )}
140
- </Modal>
141
- </>
142
- );
143
- }
144
-
145
- export function ResourceBatchActions({
146
- code,
147
- name,
148
- surface,
149
- rows,
150
- writableFields,
151
- canDelete,
152
- onCompleted,
153
- onClear,
154
- }: {
155
- code: string;
156
- name: string;
157
- surface: DataResourceSurface;
158
- rows: ResourceRecord[];
159
- writableFields: SurfaceField[];
160
- canDelete: boolean;
161
- onCompleted: () => Promise<unknown> | unknown;
162
- onClear: () => void;
163
- }) {
164
- const { message } = App.useApp();
165
- const [form] = Form.useForm();
166
- const [updateOpen, setUpdateOpen] = useState(false);
167
- const [deleteOpen, setDeleteOpen] = useState(false);
168
- const [submitting, setSubmitting] = useState(false);
169
- const [selectedField, setSelectedField] = useState('');
170
- const [idempotencyKey, setIdempotencyKey] = useState('');
171
- if (!rows.length) return null;
172
- const submitUpdate = async (values: Record<string, unknown>) => {
173
- const field = writableFields.find((item) => item.key === selectedField);
174
- if (!field) return;
175
- const value = normalizeValue(field, values[selectedField]);
176
- const changed = rows.filter((row) => !sameValue(row[selectedField], value));
177
- if (!changed.length) {
178
- message.info('所选记录的该字段已经是目标值');
179
- return;
180
- }
181
- setSubmitting(true);
182
- try {
183
- const key = idempotencyKey || crypto.randomUUID();
184
- if (!idempotencyKey) setIdempotencyKey(key);
185
- await transactNativeData(
186
- changed.map((row) => ({
187
- operation: 'update',
188
- resourceCode: code,
189
- id: row.id,
190
- expectedRevision: row.revision,
191
- data: { [selectedField]: value },
192
- })),
193
- key
194
- );
195
- message.success(`已更新 ${changed.length} 条${name}`);
196
- await onCompleted();
197
- onClear();
198
- setUpdateOpen(false);
199
- setIdempotencyKey('');
200
- form.resetFields();
201
- setSelectedField('');
202
- } catch (error) {
203
- message.error(transactionFailureMessage(error, '批量更新失败'));
204
- } finally {
205
- setSubmitting(false);
206
- }
207
- };
208
- const submitDelete = async () => {
209
- setSubmitting(true);
210
- try {
211
- const key = idempotencyKey || crypto.randomUUID();
212
- if (!idempotencyKey) setIdempotencyKey(key);
213
- await transactNativeData(
214
- rows.map((row) => ({
215
- operation: 'delete',
216
- resourceCode: code,
217
- id: row.id,
218
- expectedRevision: row.revision,
219
- })),
220
- key
221
- );
222
- message.success(`已删除 ${rows.length} 条${name}`);
223
- await onCompleted();
224
- onClear();
225
- setDeleteOpen(false);
226
- setIdempotencyKey('');
227
- } catch (error) {
228
- message.error(transactionFailureMessage(error, '批量删除失败'));
229
- } finally {
230
- setSubmitting(false);
231
- }
232
- };
233
- const field = writableFields.find((item) => item.key === selectedField);
234
- return (
235
- <>
236
- <div className="oxa-batch-bar">
237
- <Typography.Text>已选择 {rows.length} 条</Typography.Text>
238
- <Space>
239
- {writableFields.length > 0 && (
240
- <Button icon={<EditOutlined />} onClick={() => setUpdateOpen(true)}>
241
- 批量修改
242
- </Button>
243
- )}
244
- {canDelete && (
245
- <Button danger icon={<DeleteOutlined />} onClick={() => setDeleteOpen(true)}>
246
- 批量删除
247
- </Button>
248
- )}
249
- <Button onClick={onClear} type="link">
250
- 取消选择
251
- </Button>
252
- </Space>
253
- </div>
254
- <Modal
255
- cancelText="取消"
256
- okText="确认修改"
257
- open={updateOpen}
258
- title={`批量修改 ${rows.length} 条${name}`}
259
- confirmLoading={submitting}
260
- onCancel={() => !submitting && setUpdateOpen(false)}
261
- onOk={() => form.submit()}
262
- >
263
- <Form
264
- form={form}
265
- layout="vertical"
266
- onFinish={(values) => void submitUpdate(values as Record<string, unknown>)}
267
- onValuesChange={() => setIdempotencyKey('')}
268
- >
269
- <Form.Item label="修改字段" required>
270
- <Select
271
- options={writableFields.map((item) => ({
272
- label: item.label,
273
- value: item.key,
274
- }))}
275
- placeholder="选择要统一修改的字段"
276
- value={selectedField || undefined}
277
- onChange={(value) => {
278
- form.resetFields();
279
- setSelectedField(value);
280
- setIdempotencyKey('');
281
- }}
282
- />
283
- </Form.Item>
284
- {field && (
285
- <SurfaceFieldControl
286
- disabled={false}
287
- field={{ ...field, requiredHint: true }}
288
- operation="update"
289
- resourceCode={code}
290
- />
291
- )}
292
- </Form>
293
- </Modal>
294
- <Modal
295
- cancelText="取消"
296
- okButtonProps={{ danger: true }}
297
- okText="确认删除"
298
- open={deleteOpen}
299
- title={`删除 ${rows.length} 条${name}`}
300
- confirmLoading={submitting}
301
- onCancel={() => !submitting && setDeleteOpen(false)}
302
- onOk={() => void submitDelete()}
303
- >
304
- <Alert description="任意一条记录发生权限或版本冲突时,本次删除将全部回滚。" message="删除后不可恢复" showIcon type="warning" />
305
- </Modal>
306
- </>
307
- );
308
- }
309
-
310
- function normalizeValue(field: SurfaceField, value: unknown) {
311
- if (field.widget !== 'date' && field.widget !== 'datetime') return value;
312
- if (!value) return value;
313
- return field.widget === 'datetime' ? dayjs(value as never).toISOString() : dayjs(value as never).format('YYYY-MM-DD');
314
- }
315
-
316
- function sameValue(left: unknown, right: unknown) {
317
- if (left === right) return true;
318
- if (left === undefined || right === undefined) return false;
319
- return JSON.stringify(left) === JSON.stringify(right);
320
- }
321
-
322
- function transactionFailureMessage(error: unknown, fallback: string) {
323
- const index = Number((error as any)?.data?.operationIndex);
324
- const message = error instanceof Error ? error.message : fallback;
325
- return Number.isSafeInteger(index) && index >= 0 ? `第 ${index + 1} 条操作失败:${message}` : message;
326
- }