openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.89
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 +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +14 -1
- package/template/apps/server/test/smoke.test.ts +3 -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/openxiangda.config.ts +1 -1
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +95 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -35,8 +35,13 @@ import {
|
|
|
35
35
|
type MenuProps,
|
|
36
36
|
type TableColumnsType,
|
|
37
37
|
} from 'antd';
|
|
38
|
-
import
|
|
39
|
-
|
|
38
|
+
import type {
|
|
39
|
+
DataAuditEntry,
|
|
40
|
+
DataResourceSurface,
|
|
41
|
+
DataTransactionOperation,
|
|
42
|
+
SubjectProfile,
|
|
43
|
+
UserReferenceValue,
|
|
44
|
+
} from 'openxiangda-contracts/browser';
|
|
40
45
|
import { useEffect, useMemo, useState } from 'react';
|
|
41
46
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
42
47
|
import { resourceDefinitions } from '../../../../../packages/contracts/src/generated.js';
|
|
@@ -56,10 +61,24 @@ import {
|
|
|
56
61
|
SurfaceFilterControl,
|
|
57
62
|
type SurfaceField,
|
|
58
63
|
} from './SurfaceFields';
|
|
59
|
-
import {
|
|
60
|
-
|
|
64
|
+
import {
|
|
65
|
+
createNativeResourceClient,
|
|
66
|
+
transactNativeData,
|
|
67
|
+
type GenericResourceQuery,
|
|
68
|
+
} from '../../platform-client';
|
|
69
|
+
import { normalizeFormValues, normalizeRecordForForm } from '../platform-fields/field-form-codec';
|
|
70
|
+
import { DirectoryIdentityText } from '../../AuthoritativeSelector';
|
|
61
71
|
import { useRuntime } from '../../runtime';
|
|
62
72
|
import { ResourceBatchActions, ResourceImportButton } from './ResourceBatchActions';
|
|
73
|
+
import {
|
|
74
|
+
SubtableField,
|
|
75
|
+
SubtableValueDisplay,
|
|
76
|
+
} from '../platform-fields/SubtableField';
|
|
77
|
+
import {
|
|
78
|
+
buildSubtableOperations,
|
|
79
|
+
type SubtableDraftRow,
|
|
80
|
+
} from '../platform-fields/subtable-value';
|
|
81
|
+
import type { GeneratedResourceDefinition } from './generated-resource-definition';
|
|
63
82
|
|
|
64
83
|
type ResourceRecord = Record<string, unknown> & {
|
|
65
84
|
id: string;
|
|
@@ -67,19 +86,28 @@ type ResourceRecord = Record<string, unknown> & {
|
|
|
67
86
|
updated_at?: string;
|
|
68
87
|
};
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
read: string;
|
|
75
|
-
create: string;
|
|
76
|
-
update: string;
|
|
77
|
-
delete: string;
|
|
78
|
-
};
|
|
79
|
-
surface: DataResourceSurface;
|
|
80
|
-
};
|
|
89
|
+
const definitions = resourceDefinitions as unknown as Record<
|
|
90
|
+
string,
|
|
91
|
+
GeneratedResourceDefinition
|
|
92
|
+
>;
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
function signatureSigner(profile: SubjectProfile | null): UserReferenceValue | undefined {
|
|
95
|
+
if (!profile) return undefined;
|
|
96
|
+
return {
|
|
97
|
+
label: profile.displayName,
|
|
98
|
+
value: profile.userId,
|
|
99
|
+
...(profile.avatarUrl ? { avatarUrl: profile.avatarUrl } : {}),
|
|
100
|
+
...(profile.jobNumber ? { employeeNo: profile.jobNumber } : {}),
|
|
101
|
+
...(profile.affiliatedDepartment
|
|
102
|
+
? {
|
|
103
|
+
departments: [{
|
|
104
|
+
label: profile.affiliatedDepartment.name,
|
|
105
|
+
value: profile.affiliatedDepartment.id,
|
|
106
|
+
}],
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
83
111
|
|
|
84
112
|
function definitionFor(resourceCode: string) {
|
|
85
113
|
return definitions[resourceCode];
|
|
@@ -92,6 +120,7 @@ function fieldFor(surface: DataResourceSurface, key: string): SurfaceField {
|
|
|
92
120
|
: {
|
|
93
121
|
key,
|
|
94
122
|
label: key,
|
|
123
|
+
type: 'text.short',
|
|
95
124
|
widget: 'text',
|
|
96
125
|
readCapabilities: [],
|
|
97
126
|
createCapabilities: [],
|
|
@@ -112,39 +141,16 @@ function fieldsBySection(surface: DataResourceSurface) {
|
|
|
112
141
|
}));
|
|
113
142
|
}
|
|
114
143
|
|
|
115
|
-
function
|
|
116
|
-
return Object.fromEntries(
|
|
117
|
-
Object.entries(record).map(([key, value]) => {
|
|
118
|
-
const widget = surface.fields[key]?.widget;
|
|
119
|
-
return widget === 'date' || widget === 'datetime' ? [key, value ? dayjs(String(value)) : undefined] : [key, value];
|
|
120
|
-
})
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function normalizeFormValues(values: Record<string, unknown>, surface: DataResourceSurface) {
|
|
125
|
-
return Object.fromEntries(
|
|
126
|
-
Object.entries(values).map(([key, value]) => {
|
|
127
|
-
const widget = surface.fields[key]?.widget;
|
|
128
|
-
return widget === 'date' || widget === 'datetime'
|
|
129
|
-
? [
|
|
130
|
-
key,
|
|
131
|
-
value && typeof value === 'object' && 'format' in value
|
|
132
|
-
? widget === 'datetime'
|
|
133
|
-
? dayjs(value as never).toISOString()
|
|
134
|
-
: (value as { format: (pattern: string) => string }).format('YYYY-MM-DD')
|
|
135
|
-
: value,
|
|
136
|
-
]
|
|
137
|
-
: [key, value];
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function fieldWritable(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
|
|
144
|
+
function fieldWriteAuthorized(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
|
|
143
145
|
if (isAppSuperAdmin) return true;
|
|
144
146
|
const capabilities = mode === 'create' ? field.createCapabilities : field.updateCapabilities;
|
|
145
147
|
return capabilities.length > 0 && capabilities.every(hasCapability);
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
function fieldWritable(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
|
|
151
|
+
return field.widget !== 'readonly' && fieldWriteAuthorized(field, mode, hasCapability, isAppSuperAdmin);
|
|
152
|
+
}
|
|
153
|
+
|
|
148
154
|
function fieldReadable(field: SurfaceField, hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
|
|
149
155
|
return isAppSuperAdmin || (field.readCapabilities.length > 0 && field.readCapabilities.every(hasCapability));
|
|
150
156
|
}
|
|
@@ -166,27 +172,6 @@ function auditChangeFields(entry: DataAuditEntry, surface: DataResourceSurface)
|
|
|
166
172
|
return fields.filter((field) => Object.prototype.hasOwnProperty.call(source, field.key));
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
function exportCellValue(field: SurfaceField, value: unknown) {
|
|
170
|
-
if (value === undefined || value === null || value === '') return '';
|
|
171
|
-
if (field.widget === 'boolean') return value ? '是' : '否';
|
|
172
|
-
if (field.options && !Array.isArray(value)) {
|
|
173
|
-
return field.options.find((option) => option.value === value)?.label || String(value);
|
|
174
|
-
}
|
|
175
|
-
if (Array.isArray(value)) {
|
|
176
|
-
return value.map((item) => field.options?.find((option) => option.value === item)?.label || String(item)).join('、');
|
|
177
|
-
}
|
|
178
|
-
if (field.widget === 'date' || field.widget === 'datetime') {
|
|
179
|
-
const parsed = new Date(String(value));
|
|
180
|
-
if (!Number.isNaN(parsed.getTime())) return parsed.toLocaleString('zh-CN', { hour12: false });
|
|
181
|
-
}
|
|
182
|
-
if (typeof value === 'object' && value && 'id' in value) return String((value as { id: unknown }).id);
|
|
183
|
-
return String(value);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function csvCell(value: string) {
|
|
187
|
-
return /[\r\n,"]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
175
|
async function exportResourceRows(
|
|
191
176
|
code: string,
|
|
192
177
|
name: string,
|
|
@@ -196,35 +181,21 @@ async function exportResourceRows(
|
|
|
196
181
|
sort: { field: string; order: 'asc' | 'desc' }
|
|
197
182
|
) {
|
|
198
183
|
const client = createNativeResourceClient(code, surface);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
searchField: query.searchField,
|
|
209
|
-
filters: query.filters,
|
|
210
|
-
sort,
|
|
211
|
-
});
|
|
212
|
-
rows.push(...(result.rows as ResourceRecord[]));
|
|
213
|
-
total = result.total;
|
|
214
|
-
page += 1;
|
|
215
|
-
} while (rows.length < total && rows.length < 10000 && page <= 20);
|
|
216
|
-
const header = fields.map((field) => csvCell(field.label)).join(',');
|
|
217
|
-
const body = rows.map((row) => fields.map((field) => csvCell(exportCellValue(field, row[field.key]))).join(','));
|
|
218
|
-
const blob = new Blob([`\uFEFF${[header, ...body].join('\r\n')}`], {
|
|
219
|
-
type: 'text/csv;charset=utf-8',
|
|
220
|
-
});
|
|
221
|
-
const url = URL.createObjectURL(blob);
|
|
184
|
+
const exported = await client.exportCsv({
|
|
185
|
+
page: 1,
|
|
186
|
+
pageSize: 200,
|
|
187
|
+
keyword: query.keyword,
|
|
188
|
+
searchField: query.searchField,
|
|
189
|
+
filters: query.filters,
|
|
190
|
+
sort,
|
|
191
|
+
}, fields.map((field) => field.key));
|
|
192
|
+
const url = URL.createObjectURL(exported.blob);
|
|
222
193
|
const anchor = document.createElement('a');
|
|
223
194
|
anchor.href = url;
|
|
224
|
-
anchor.download = `${name}
|
|
195
|
+
anchor.download = exported.fileName || `${name}.csv`;
|
|
225
196
|
anchor.click();
|
|
226
|
-
URL.revokeObjectURL(url);
|
|
227
|
-
return {
|
|
197
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
198
|
+
return { rowLimit: exported.rowLimit };
|
|
228
199
|
}
|
|
229
200
|
|
|
230
201
|
function AuditEntry({ entry, surface, readable }: { entry: DataAuditEntry; surface: DataResourceSurface; readable: (field: SurfaceField) => boolean }) {
|
|
@@ -244,7 +215,7 @@ function AuditEntry({ entry, surface, readable }: { entry: DataAuditEntry; surfa
|
|
|
244
215
|
{entry.actor.oauthClientId ? `(${entry.actor.oauthClientId})` : ''}
|
|
245
216
|
</Typography.Text>
|
|
246
217
|
) : (
|
|
247
|
-
<
|
|
218
|
+
<DirectoryIdentityText id={entry.actor.userId} source="user" />
|
|
248
219
|
)}
|
|
249
220
|
<span>时间</span>
|
|
250
221
|
<Typography.Text>{new Date(entry.occurredAt).toLocaleString('zh-CN')}</Typography.Text>
|
|
@@ -273,7 +244,10 @@ function AuditEntry({ entry, surface, readable }: { entry: DataAuditEntry; surfa
|
|
|
273
244
|
);
|
|
274
245
|
}
|
|
275
246
|
|
|
276
|
-
function useGeneratedList(
|
|
247
|
+
function useGeneratedList(
|
|
248
|
+
definition: GeneratedResourceDefinition,
|
|
249
|
+
readableFieldCodes: Set<string>
|
|
250
|
+
) {
|
|
277
251
|
const { code, surface } = definition;
|
|
278
252
|
const declaredDefaultSort = surface.list?.defaultSort?.field;
|
|
279
253
|
const defaultSortField =
|
|
@@ -328,12 +302,14 @@ export function GeneratedResourcePage({
|
|
|
328
302
|
resourceCode,
|
|
329
303
|
mode = 'list',
|
|
330
304
|
variant = 'desktop',
|
|
305
|
+
definition: providedDefinition,
|
|
331
306
|
}: {
|
|
332
307
|
resourceCode: string;
|
|
333
308
|
mode?: 'list' | 'create' | 'edit' | 'detail';
|
|
334
309
|
variant?: 'desktop' | 'mobile';
|
|
310
|
+
definition?: GeneratedResourceDefinition;
|
|
335
311
|
}) {
|
|
336
|
-
const definition = definitionFor(resourceCode);
|
|
312
|
+
const definition = providedDefinition || definitionFor(resourceCode);
|
|
337
313
|
if (!definition || definition.surface.generated === false) {
|
|
338
314
|
return <Result status="404" title="未声明可生成的资源页面" />;
|
|
339
315
|
}
|
|
@@ -343,11 +319,11 @@ export function GeneratedResourcePage({
|
|
|
343
319
|
return <GeneratedResourceFormPage key={pageKey} definition={definition} mode={mode} variant={variant} />;
|
|
344
320
|
}
|
|
345
321
|
|
|
346
|
-
function GeneratedResourceListPage({ definition, variant }: { definition:
|
|
322
|
+
function GeneratedResourceListPage({ definition, variant }: { definition: GeneratedResourceDefinition; variant: 'desktop' | 'mobile' }) {
|
|
347
323
|
return variant === 'mobile' ? <GeneratedMobileList definition={definition} /> : <GeneratedDesktopList definition={definition} />;
|
|
348
324
|
}
|
|
349
325
|
|
|
350
|
-
function GeneratedDesktopList({ definition }: { definition:
|
|
326
|
+
function GeneratedDesktopList({ definition }: { definition: GeneratedResourceDefinition }) {
|
|
351
327
|
const navigate = useNavigate();
|
|
352
328
|
const { message } = App.useApp();
|
|
353
329
|
const { hasCapability, identity } = useRuntime();
|
|
@@ -398,6 +374,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
398
374
|
render: (value: unknown, record: ResourceRecord) => (
|
|
399
375
|
<SurfaceFieldValue
|
|
400
376
|
field={field}
|
|
377
|
+
resourceCode={code}
|
|
401
378
|
value={value}
|
|
402
379
|
renderers={{
|
|
403
380
|
renderValue: ({ value: current }) =>
|
|
@@ -474,7 +451,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
474
451
|
setExporting(true);
|
|
475
452
|
try {
|
|
476
453
|
const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
|
|
477
|
-
message.success(
|
|
454
|
+
message.success(`导出已生成(最多 ${result.rowLimit} 条)`);
|
|
478
455
|
} catch (error) {
|
|
479
456
|
message.error(error instanceof Error ? error.message : '导出失败');
|
|
480
457
|
} finally {
|
|
@@ -569,6 +546,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
569
546
|
<div className={`oxa-filter-item${index >= 2 && !filtersExpanded ? ' is-collapsed' : ''}`} key={key}>
|
|
570
547
|
<SurfaceFilterControl
|
|
571
548
|
field={field}
|
|
549
|
+
resourceCode={code}
|
|
572
550
|
onChange={(value) =>
|
|
573
551
|
state.setDraft((item) => ({
|
|
574
552
|
...item,
|
|
@@ -708,7 +686,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
708
686
|
);
|
|
709
687
|
}
|
|
710
688
|
|
|
711
|
-
function GeneratedMobileList({ definition }: { definition:
|
|
689
|
+
function GeneratedMobileList({ definition }: { definition: GeneratedResourceDefinition }) {
|
|
712
690
|
const navigate = useNavigate();
|
|
713
691
|
const { hasCapability, identity } = useRuntime();
|
|
714
692
|
const { message } = App.useApp();
|
|
@@ -728,7 +706,7 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
|
|
|
728
706
|
setExporting(true);
|
|
729
707
|
try {
|
|
730
708
|
const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
|
|
731
|
-
message.success(
|
|
709
|
+
message.success(`导出已生成(最多 ${result.rowLimit} 条)`);
|
|
732
710
|
} catch (error) {
|
|
733
711
|
message.error(error instanceof Error ? error.message : '导出失败');
|
|
734
712
|
} finally {
|
|
@@ -798,7 +776,7 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
|
|
|
798
776
|
<div key={field.key}>
|
|
799
777
|
<Typography.Text type="secondary">{field.label}</Typography.Text>
|
|
800
778
|
<div>
|
|
801
|
-
<SurfaceFieldValue field={field} value={record[field.key]} />
|
|
779
|
+
<SurfaceFieldValue field={field} resourceCode={code} value={record[field.key]} />
|
|
802
780
|
</div>
|
|
803
781
|
</div>
|
|
804
782
|
))}
|
|
@@ -839,7 +817,7 @@ function GeneratedResourceFormPage({
|
|
|
839
817
|
mode,
|
|
840
818
|
variant,
|
|
841
819
|
}: {
|
|
842
|
-
definition:
|
|
820
|
+
definition: GeneratedResourceDefinition;
|
|
843
821
|
mode: 'create' | 'edit';
|
|
844
822
|
variant: 'desktop' | 'mobile';
|
|
845
823
|
}) {
|
|
@@ -850,6 +828,7 @@ function GeneratedResourceFormPage({
|
|
|
850
828
|
const [form] = Form.useForm();
|
|
851
829
|
const create = useCreate<ResourceRecord>();
|
|
852
830
|
const update = useUpdate<ResourceRecord>();
|
|
831
|
+
const [transactionPending, setTransactionPending] = useState(false);
|
|
853
832
|
const query = useOne<ResourceRecord>({
|
|
854
833
|
resource: definition.code,
|
|
855
834
|
id,
|
|
@@ -867,19 +846,102 @@ function GeneratedResourceFormPage({
|
|
|
867
846
|
if (!hasCapability(mode === 'create' ? capabilities.create : capabilities.update))
|
|
868
847
|
return <Result status="403" title="当前平台用户无此操作权限" />;
|
|
869
848
|
const submit = async (values: Record<string, unknown>) => {
|
|
870
|
-
const
|
|
871
|
-
Object.
|
|
872
|
-
|
|
849
|
+
const writableValues = Object.fromEntries(
|
|
850
|
+
Object.entries(values).filter(([key]) =>
|
|
851
|
+
fieldWritable(
|
|
852
|
+
fieldFor(surface, key),
|
|
853
|
+
mode,
|
|
854
|
+
hasCapability,
|
|
855
|
+
identity.isAppSuperAdmin
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
);
|
|
859
|
+
const subtableFields = Object.entries(surface.fields)
|
|
860
|
+
.map(([key, field]) => ({ key, ...field }))
|
|
861
|
+
.filter(
|
|
862
|
+
field =>
|
|
863
|
+
field.type === 'subtable' &&
|
|
873
864
|
fieldWritable(
|
|
874
|
-
|
|
865
|
+
field,
|
|
875
866
|
mode,
|
|
876
867
|
hasCapability,
|
|
877
868
|
identity.isAppSuperAdmin
|
|
878
869
|
)
|
|
870
|
+
);
|
|
871
|
+
const next = normalizeFormValues(
|
|
872
|
+
Object.fromEntries(
|
|
873
|
+
Object.entries(writableValues).filter(
|
|
874
|
+
([key]) => surface.fields[key]?.type !== 'subtable'
|
|
879
875
|
)
|
|
880
876
|
),
|
|
881
877
|
surface
|
|
882
878
|
);
|
|
879
|
+
if (subtableFields.length > 0) {
|
|
880
|
+
if (mode === 'edit' && (!record?.id || !Number.isSafeInteger(record.revision))) {
|
|
881
|
+
throw new Error('OPENXIANGDA_SUBTABLE_PARENT_REVISION_REQUIRED');
|
|
882
|
+
}
|
|
883
|
+
const operations: DataTransactionOperation[] = [
|
|
884
|
+
mode === 'create'
|
|
885
|
+
? { operation: 'create', resourceCode: code, data: next }
|
|
886
|
+
: {
|
|
887
|
+
operation: 'update',
|
|
888
|
+
resourceCode: code,
|
|
889
|
+
id: record!.id,
|
|
890
|
+
expectedRevision: record!.revision,
|
|
891
|
+
data: next,
|
|
892
|
+
},
|
|
893
|
+
];
|
|
894
|
+
for (const subtableField of subtableFields) {
|
|
895
|
+
const config = subtableField.subtable;
|
|
896
|
+
const childDefinition = config
|
|
897
|
+
? definitionFor(config.resourceCode)
|
|
898
|
+
: undefined;
|
|
899
|
+
if (!config || !childDefinition) {
|
|
900
|
+
throw new Error(`OPENXIANGDA_SUBTABLE_DEFINITION_MISSING:${subtableField.key}`);
|
|
901
|
+
}
|
|
902
|
+
operations.push(
|
|
903
|
+
...buildSubtableOperations({
|
|
904
|
+
rows: Array.isArray(writableValues[subtableField.key])
|
|
905
|
+
? writableValues[subtableField.key] as SubtableDraftRow[]
|
|
906
|
+
: [],
|
|
907
|
+
childResourceCode: config.resourceCode,
|
|
908
|
+
childSurface: childDefinition.surface,
|
|
909
|
+
foreignKey: config.foreignKey,
|
|
910
|
+
orderField: config.orderField,
|
|
911
|
+
maxRows: config.maxRows,
|
|
912
|
+
parent: mode === 'create'
|
|
913
|
+
? { operation: 'create', operationIndex: 0 }
|
|
914
|
+
: { operation: 'update', id: record!.id },
|
|
915
|
+
canWrite: (fieldCode, childField, childOperation) =>
|
|
916
|
+
fieldWriteAuthorized(
|
|
917
|
+
{ key: fieldCode, ...childField },
|
|
918
|
+
childOperation === 'create' ? 'create' : 'edit',
|
|
919
|
+
hasCapability,
|
|
920
|
+
identity.isAppSuperAdmin
|
|
921
|
+
),
|
|
922
|
+
canDelete:
|
|
923
|
+
identity.isAppSuperAdmin ||
|
|
924
|
+
hasCapability(childDefinition.capabilities.delete),
|
|
925
|
+
})
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
if (operations.length > 100) {
|
|
929
|
+
throw new Error('OPENXIANGDA_DATA_TRANSACTION_TOO_LARGE');
|
|
930
|
+
}
|
|
931
|
+
setTransactionPending(true);
|
|
932
|
+
try {
|
|
933
|
+
const result = await transactNativeData(operations);
|
|
934
|
+
const parent = result.items[0];
|
|
935
|
+
if (!parent || parent.operation === 'emitEvent') {
|
|
936
|
+
throw new Error('OPENXIANGDA_SUBTABLE_PARENT_RESULT_MISSING');
|
|
937
|
+
}
|
|
938
|
+
message.success(mode === 'create' ? `${name}已创建` : '修改已保存');
|
|
939
|
+
navigate(mode === 'create' ? listPath : `/${code}/${parent.id}`);
|
|
940
|
+
} finally {
|
|
941
|
+
setTransactionPending(false);
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
883
945
|
if (mode === 'create') {
|
|
884
946
|
await create.mutateAsync({ resource: code, values: next });
|
|
885
947
|
message.success(`${name}已创建`);
|
|
@@ -931,8 +993,19 @@ function GeneratedResourceFormPage({
|
|
|
931
993
|
disabled={!fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)}
|
|
932
994
|
field={field}
|
|
933
995
|
operation={mode === 'create' ? 'create' : 'update'}
|
|
996
|
+
resourceCode={code}
|
|
934
997
|
recordId={record?.id}
|
|
935
998
|
renderers={{
|
|
999
|
+
renderSubtable: (context) => (
|
|
1000
|
+
<SubtableField
|
|
1001
|
+
disabled={context.disabled}
|
|
1002
|
+
field={context.field}
|
|
1003
|
+
mobile={variant === 'mobile'}
|
|
1004
|
+
operation={context.operation}
|
|
1005
|
+
parentRecordId={context.recordId}
|
|
1006
|
+
/>
|
|
1007
|
+
),
|
|
1008
|
+
signer: signatureSigner(identity.subjectProfile),
|
|
936
1009
|
upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId),
|
|
937
1010
|
}}
|
|
938
1011
|
/>
|
|
@@ -943,7 +1016,7 @@ function GeneratedResourceFormPage({
|
|
|
943
1016
|
</div>
|
|
944
1017
|
<div className="oxa-actions">
|
|
945
1018
|
<Button onClick={() => navigate(mode === 'edit' ? detailPath : listPath)}>取消</Button>
|
|
946
|
-
<Button htmlType="submit" loading={create.mutation.isPending || update.mutation.isPending} type="primary">
|
|
1019
|
+
<Button htmlType="submit" loading={create.mutation.isPending || update.mutation.isPending || transactionPending} type="primary">
|
|
947
1020
|
{mode === 'create' ? '保存' : '保存修改'}
|
|
948
1021
|
</Button>
|
|
949
1022
|
</div>
|
|
@@ -952,7 +1025,7 @@ function GeneratedResourceFormPage({
|
|
|
952
1025
|
);
|
|
953
1026
|
}
|
|
954
1027
|
|
|
955
|
-
function GeneratedResourceDetailPage({ definition, variant }: { definition:
|
|
1028
|
+
function GeneratedResourceDetailPage({ definition, variant }: { definition: GeneratedResourceDefinition; variant: 'desktop' | 'mobile' }) {
|
|
956
1029
|
const { id = '' } = useParams();
|
|
957
1030
|
const navigate = useNavigate();
|
|
958
1031
|
const { hasCapability, identity } = useRuntime();
|
|
@@ -1041,7 +1114,20 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
|
|
|
1041
1114
|
items={group.fields.map((field) => ({
|
|
1042
1115
|
key: field.key,
|
|
1043
1116
|
label: field.label,
|
|
1044
|
-
|
|
1117
|
+
span: field.type === 'subtable' ? 3 : 1,
|
|
1118
|
+
children: field.type === 'subtable' ? (
|
|
1119
|
+
<SubtableValueDisplay
|
|
1120
|
+
field={field}
|
|
1121
|
+
mobile={variant === 'mobile'}
|
|
1122
|
+
parentRecordId={record.id}
|
|
1123
|
+
/>
|
|
1124
|
+
) : (
|
|
1125
|
+
<SurfaceFieldValue
|
|
1126
|
+
field={field}
|
|
1127
|
+
resourceCode={definition.code}
|
|
1128
|
+
value={record[field.key]}
|
|
1129
|
+
/>
|
|
1130
|
+
),
|
|
1045
1131
|
}))}
|
|
1046
1132
|
/>
|
|
1047
1133
|
</Card>
|
|
@@ -281,7 +281,14 @@ export function ResourceBatchActions({
|
|
|
281
281
|
}}
|
|
282
282
|
/>
|
|
283
283
|
</Form.Item>
|
|
284
|
-
{field &&
|
|
284
|
+
{field && (
|
|
285
|
+
<SurfaceFieldControl
|
|
286
|
+
disabled={false}
|
|
287
|
+
field={{ ...field, requiredHint: true }}
|
|
288
|
+
operation="update"
|
|
289
|
+
resourceCode={code}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
285
292
|
</Form>
|
|
286
293
|
</Modal>
|
|
287
294
|
<Modal
|