openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.91

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 (77) 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 +3 -3
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +1 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/web/e2e/client-fixture.ts +5 -0
  20. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  21. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  22. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  23. package/template/apps/web/e2e/resources.spec.ts +20 -181
  24. package/template/apps/web/field-protocol.e2e.html +3 -0
  25. package/template/apps/web/package.json +2 -5
  26. package/template/apps/web/src/main.tsx +12 -60
  27. package/template/apps/web/test/contracts.test.ts +52 -104
  28. package/template/openxiangda.config.ts +1 -1
  29. package/template/package.json +1 -3
  30. package/template/packages/contracts/src/generated.ts +1 -0
  31. package/template/scripts/verify-template-budget.mjs +2 -2
  32. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  33. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  34. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  35. package/template/apps/web/src/Shell.tsx +0 -445
  36. package/template/apps/web/src/appearance.tsx +0 -147
  37. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  38. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  39. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  40. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  41. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  42. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  43. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  44. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  45. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  46. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  47. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  48. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  49. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  50. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  51. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  52. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  53. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  54. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  55. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  56. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  57. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  58. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  59. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  60. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  61. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  62. package/template/apps/web/src/data-provider.ts +0 -72
  63. package/template/apps/web/src/platform-client.ts +0 -708
  64. package/template/apps/web/src/runtime-meta.ts +0 -75
  65. package/template/apps/web/src/runtime.tsx +0 -227
  66. package/template/apps/web/src/styles.css +0 -1941
  67. package/template/apps/web/test/address-field.test.ts +0 -66
  68. package/template/apps/web/test/cascade-value.test.ts +0 -68
  69. package/template/apps/web/test/directory-value.test.ts +0 -55
  70. package/template/apps/web/test/field-bounds.test.ts +0 -17
  71. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  72. package/template/apps/web/test/location-field.test.ts +0 -83
  73. package/template/apps/web/test/resource-import.test.ts +0 -143
  74. package/template/apps/web/test/resource-query.test.ts +0 -144
  75. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  76. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  77. 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
- }