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
|
@@ -4,10 +4,15 @@ import {
|
|
|
4
4
|
UserOutlined,
|
|
5
5
|
} from '@ant-design/icons';
|
|
6
6
|
import { Avatar, Select, Space, Spin, Tag, Typography } from 'antd';
|
|
7
|
+
import type {
|
|
8
|
+
DepartmentReferenceValue,
|
|
9
|
+
DirectoryEntry,
|
|
10
|
+
ResourceReferenceValue,
|
|
11
|
+
UserReferenceValue,
|
|
12
|
+
} from 'openxiangda-contracts/browser';
|
|
7
13
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
8
14
|
import {
|
|
9
15
|
resolveDirectory,
|
|
10
|
-
resolveResource,
|
|
11
16
|
searchDirectory,
|
|
12
17
|
searchResource,
|
|
13
18
|
type DirectoryKind,
|
|
@@ -21,48 +26,105 @@ type Option = {
|
|
|
21
26
|
description?: string;
|
|
22
27
|
path?: string[];
|
|
23
28
|
source: Source;
|
|
29
|
+
reference?: StoredValue;
|
|
24
30
|
};
|
|
25
31
|
|
|
32
|
+
type StoredValue =
|
|
33
|
+
| UserReferenceValue
|
|
34
|
+
| DepartmentReferenceValue
|
|
35
|
+
| ResourceReferenceValue;
|
|
36
|
+
|
|
26
37
|
function mergeOptions(current: Option[], incoming: Option[]) {
|
|
27
38
|
const merged = new Map(current.map(option => [option.value, option]));
|
|
28
39
|
for (const option of incoming) merged.set(option.value, option);
|
|
29
40
|
return [...merged.values()];
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
function directoryOption(
|
|
33
|
-
source: DirectoryKind,
|
|
34
|
-
item: {
|
|
35
|
-
id: string;
|
|
36
|
-
label: string;
|
|
37
|
-
selectable: boolean;
|
|
38
|
-
description?: string;
|
|
39
|
-
path?: Array<{ label: string }>;
|
|
40
|
-
}
|
|
41
|
-
): Option {
|
|
43
|
+
function directoryOption(source: DirectoryKind, item: DirectoryEntry): Option {
|
|
42
44
|
return {
|
|
43
45
|
source,
|
|
44
46
|
value: item.id,
|
|
45
47
|
label: item.label,
|
|
46
|
-
selectable: item.selectable,
|
|
48
|
+
selectable: item.selectable !== false,
|
|
47
49
|
...(item.description ? { description: item.description } : {}),
|
|
48
50
|
...(item.path?.length
|
|
49
51
|
? { path: item.path.map(entry => entry.label) }
|
|
50
52
|
: {}),
|
|
53
|
+
reference: item.snapshot,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resourceOption(item: ResourceReferenceValue): Option {
|
|
58
|
+
return {
|
|
59
|
+
source: 'resource',
|
|
60
|
+
value: item.value,
|
|
61
|
+
label: item.label,
|
|
62
|
+
selectable: true,
|
|
63
|
+
...(item.description ? { description: item.description } : {}),
|
|
64
|
+
reference: item,
|
|
51
65
|
};
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
function storedValues(value: StoredValue | StoredValue[] | undefined) {
|
|
69
|
+
return (Array.isArray(value) ? value : value ? [value] : []).map(item => item.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function storedReferenceOptions(
|
|
73
|
+
source: Source,
|
|
74
|
+
value: StoredValue | StoredValue[] | undefined
|
|
75
|
+
) {
|
|
76
|
+
return (Array.isArray(value) ? value : value ? [value] : []).map(item =>
|
|
77
|
+
source === 'resource'
|
|
78
|
+
? resourceOption(item as ResourceReferenceValue)
|
|
79
|
+
: {
|
|
80
|
+
source,
|
|
81
|
+
value: item.value,
|
|
82
|
+
label: item.label,
|
|
83
|
+
selectable: true,
|
|
84
|
+
...(source === 'user' && (item as UserReferenceValue).employeeNo
|
|
85
|
+
? { description: (item as UserReferenceValue).employeeNo }
|
|
86
|
+
: {}),
|
|
87
|
+
...(source === 'department' && (item as DepartmentReferenceValue).fullPath
|
|
88
|
+
? { description: (item as DepartmentReferenceValue).fullPath }
|
|
89
|
+
: {}),
|
|
90
|
+
reference: item,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function selectedValue(
|
|
96
|
+
source: Source,
|
|
97
|
+
ids: string | string[] | undefined,
|
|
98
|
+
options: Option[],
|
|
99
|
+
multiple: boolean
|
|
100
|
+
) {
|
|
101
|
+
const values = (Array.isArray(ids) ? ids : ids ? [ids] : [])
|
|
102
|
+
.map(id => options.find(option => option.value === id)?.reference)
|
|
103
|
+
.filter((item): item is StoredValue => Boolean(item));
|
|
104
|
+
return multiple ? values : values[0];
|
|
105
|
+
}
|
|
106
|
+
|
|
54
107
|
async function searchSource(
|
|
55
108
|
source: Source,
|
|
56
109
|
operation: 'create' | 'update',
|
|
57
110
|
keyword: string,
|
|
58
111
|
cursor?: string,
|
|
59
112
|
resourceCode?: string,
|
|
60
|
-
|
|
113
|
+
fieldCode?: string,
|
|
114
|
+
bindings?: Record<string, unknown>,
|
|
61
115
|
) {
|
|
62
116
|
if (source === 'resource') {
|
|
63
|
-
if (!resourceCode || !
|
|
64
|
-
const page = await searchResource(resourceCode,
|
|
65
|
-
|
|
117
|
+
if (!resourceCode || !fieldCode) throw new Error('OPENXIANGDA_RESOURCE_REFERENCE_CONFIG_INVALID');
|
|
118
|
+
const page = await searchResource(resourceCode, fieldCode, {
|
|
119
|
+
operation,
|
|
120
|
+
keyword,
|
|
121
|
+
cursor,
|
|
122
|
+
bindings,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
items: page.items.map(resourceOption),
|
|
126
|
+
nextCursor: page.nextCursor,
|
|
127
|
+
};
|
|
66
128
|
}
|
|
67
129
|
const page = await searchDirectory(source, { keyword, cursor });
|
|
68
130
|
return {
|
|
@@ -71,22 +133,6 @@ async function searchSource(
|
|
|
71
133
|
};
|
|
72
134
|
}
|
|
73
135
|
|
|
74
|
-
async function resolveSource(
|
|
75
|
-
source: Source,
|
|
76
|
-
operation: 'create' | 'update',
|
|
77
|
-
values: string[],
|
|
78
|
-
resourceCode?: string,
|
|
79
|
-
labelField?: string,
|
|
80
|
-
) {
|
|
81
|
-
if (source === 'resource') {
|
|
82
|
-
if (!resourceCode || !labelField) throw new Error('OPENXIANGDA_RESOURCE_REFERENCE_CONFIG_INVALID');
|
|
83
|
-
return (await resolveResource(resourceCode, values, labelField)).items.map(item => ({ ...item, value: item.id, source }) as Option);
|
|
84
|
-
}
|
|
85
|
-
return (await resolveDirectory(source, values)).items.map(item =>
|
|
86
|
-
directoryOption(source, item)
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
136
|
function optionIcon(option: Pick<Option, 'source' | 'label'>) {
|
|
91
137
|
if (option.source === 'user') {
|
|
92
138
|
return (
|
|
@@ -108,16 +154,18 @@ function optionDescription(option: Option) {
|
|
|
108
154
|
}
|
|
109
155
|
|
|
110
156
|
export type AuthoritativeSelectorProps = {
|
|
111
|
-
value?:
|
|
112
|
-
onChange?: (value:
|
|
157
|
+
value?: StoredValue | StoredValue[];
|
|
158
|
+
onChange?: (value: StoredValue | StoredValue[] | undefined) => void;
|
|
113
159
|
source: Source;
|
|
114
160
|
operation: 'create' | 'update';
|
|
115
161
|
multiple?: boolean;
|
|
116
162
|
disabled?: boolean;
|
|
117
163
|
id?: string;
|
|
118
164
|
placeholder: string;
|
|
165
|
+
/** Host resource and field; the platform resolves the target source declaration. */
|
|
119
166
|
resourceCode?: string;
|
|
120
|
-
|
|
167
|
+
fieldCode?: string;
|
|
168
|
+
bindings?: Record<string, unknown>;
|
|
121
169
|
};
|
|
122
170
|
|
|
123
171
|
export function AuthoritativeSelector({
|
|
@@ -130,42 +178,23 @@ export function AuthoritativeSelector({
|
|
|
130
178
|
id,
|
|
131
179
|
placeholder,
|
|
132
180
|
resourceCode,
|
|
133
|
-
|
|
181
|
+
fieldCode,
|
|
182
|
+
bindings,
|
|
134
183
|
}: AuthoritativeSelectorProps) {
|
|
135
|
-
const [options, setOptions] = useState<Option[]>(
|
|
184
|
+
const [options, setOptions] = useState<Option[]>(() =>
|
|
185
|
+
storedReferenceOptions(source, value)
|
|
186
|
+
);
|
|
136
187
|
const [keyword, setKeyword] = useState('');
|
|
137
188
|
const [cursor, setCursor] = useState<string | null>(null);
|
|
138
189
|
const [loading, setLoading] = useState(false);
|
|
139
190
|
const [error, setError] = useState('');
|
|
140
191
|
const [open, setOpen] = useState(false);
|
|
141
192
|
const requestSequence = useRef(0);
|
|
142
|
-
const values = useMemo(
|
|
143
|
-
() => (Array.isArray(value) ? value : value ? [value] : []),
|
|
144
|
-
[value]
|
|
145
|
-
);
|
|
193
|
+
const values = useMemo(() => storedValues(value), [value]);
|
|
146
194
|
|
|
147
195
|
useEffect(() => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
if (missing.length === 0) return;
|
|
152
|
-
let active = true;
|
|
153
|
-
setLoading(true);
|
|
154
|
-
void resolveSource(source, operation, missing, resourceCode, labelField)
|
|
155
|
-
.then(items => {
|
|
156
|
-
if (active) setOptions(current => mergeOptions(current, items));
|
|
157
|
-
})
|
|
158
|
-
.catch(reason => {
|
|
159
|
-
if (active)
|
|
160
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
161
|
-
})
|
|
162
|
-
.finally(() => {
|
|
163
|
-
if (active) setLoading(false);
|
|
164
|
-
});
|
|
165
|
-
return () => {
|
|
166
|
-
active = false;
|
|
167
|
-
};
|
|
168
|
-
}, [labelField, operation, options, resourceCode, source, values]);
|
|
196
|
+
setOptions(current => mergeOptions(current, storedReferenceOptions(source, value)));
|
|
197
|
+
}, [source, value]);
|
|
169
198
|
|
|
170
199
|
useEffect(() => {
|
|
171
200
|
if (disabled) return;
|
|
@@ -177,7 +206,7 @@ export function AuthoritativeSelector({
|
|
|
177
206
|
const timer = window.setTimeout(() => {
|
|
178
207
|
setLoading(true);
|
|
179
208
|
setError('');
|
|
180
|
-
void searchSource(source, operation, keyword, undefined, resourceCode,
|
|
209
|
+
void searchSource(source, operation, keyword, undefined, resourceCode, fieldCode, bindings)
|
|
181
210
|
.then(page => {
|
|
182
211
|
if (sequence !== requestSequence.current) return;
|
|
183
212
|
setOptions(current => {
|
|
@@ -199,14 +228,14 @@ export function AuthoritativeSelector({
|
|
|
199
228
|
});
|
|
200
229
|
}, 300);
|
|
201
230
|
return () => window.clearTimeout(timer);
|
|
202
|
-
}, [disabled,
|
|
231
|
+
}, [bindings, disabled, fieldCode, keyword, operation, resourceCode, source, values]);
|
|
203
232
|
|
|
204
233
|
useEffect(() => {
|
|
205
234
|
if (disabled || !open || keyword.trim().length >= 2) return;
|
|
206
235
|
const sequence = ++requestSequence.current;
|
|
207
236
|
setLoading(true);
|
|
208
237
|
setError('');
|
|
209
|
-
void searchSource(source, operation, '', undefined, resourceCode,
|
|
238
|
+
void searchSource(source, operation, '', undefined, resourceCode, fieldCode, bindings)
|
|
210
239
|
.then(page => {
|
|
211
240
|
if (sequence !== requestSequence.current) return;
|
|
212
241
|
setOptions(current => {
|
|
@@ -223,14 +252,14 @@ export function AuthoritativeSelector({
|
|
|
223
252
|
.finally(() => {
|
|
224
253
|
if (sequence === requestSequence.current) setLoading(false);
|
|
225
254
|
});
|
|
226
|
-
}, [disabled,
|
|
255
|
+
}, [bindings, disabled, fieldCode, keyword, open, operation, resourceCode, source, values]);
|
|
227
256
|
|
|
228
257
|
const loadNext = () => {
|
|
229
258
|
if (!cursor || loading) return;
|
|
230
259
|
const nextCursor = cursor;
|
|
231
260
|
setLoading(true);
|
|
232
261
|
setError('');
|
|
233
|
-
void searchSource(source, operation, keyword, nextCursor, resourceCode,
|
|
262
|
+
void searchSource(source, operation, keyword, nextCursor, resourceCode, fieldCode, bindings)
|
|
234
263
|
.then(page => {
|
|
235
264
|
setOptions(current => mergeOptions(current, page.items));
|
|
236
265
|
setCursor(page.nextCursor);
|
|
@@ -268,7 +297,14 @@ export function AuthoritativeSelector({
|
|
|
268
297
|
)
|
|
269
298
|
}
|
|
270
299
|
onChange={next => {
|
|
271
|
-
onChange?.(
|
|
300
|
+
onChange?.(
|
|
301
|
+
selectedValue(
|
|
302
|
+
source,
|
|
303
|
+
(next || undefined) as string | string[] | undefined,
|
|
304
|
+
options,
|
|
305
|
+
multiple
|
|
306
|
+
)
|
|
307
|
+
);
|
|
272
308
|
if (!multiple) setOpen(false);
|
|
273
309
|
}}
|
|
274
310
|
onOpenChange={next => {
|
|
@@ -343,7 +379,7 @@ export function AuthoritativeSelector({
|
|
|
343
379
|
</Tag>
|
|
344
380
|
);
|
|
345
381
|
}}
|
|
346
|
-
value={
|
|
382
|
+
value={multiple ? values : values[0]}
|
|
347
383
|
/>
|
|
348
384
|
);
|
|
349
385
|
}
|
|
@@ -364,9 +400,12 @@ export function MobileAuthoritativeSelector({
|
|
|
364
400
|
id,
|
|
365
401
|
placeholder,
|
|
366
402
|
resourceCode,
|
|
367
|
-
|
|
403
|
+
fieldCode,
|
|
404
|
+
bindings,
|
|
368
405
|
}: AuthoritativeSelectorProps) {
|
|
369
|
-
const [options, setOptions] = useState<Option[]>(
|
|
406
|
+
const [options, setOptions] = useState<Option[]>(() =>
|
|
407
|
+
storedReferenceOptions(source, value)
|
|
408
|
+
);
|
|
370
409
|
const [keyword, setKeyword] = useState('');
|
|
371
410
|
const [cursor, setCursor] = useState<string | null>(null);
|
|
372
411
|
const [loading, setLoading] = useState(false);
|
|
@@ -374,36 +413,15 @@ export function MobileAuthoritativeSelector({
|
|
|
374
413
|
const [open, setOpen] = useState(false);
|
|
375
414
|
const [draftValues, setDraftValues] = useState<string[]>([]);
|
|
376
415
|
const requestSequence = useRef(0);
|
|
377
|
-
const values = useMemo(
|
|
378
|
-
() => (Array.isArray(value) ? value : value ? [value] : []),
|
|
379
|
-
[value]
|
|
380
|
-
);
|
|
416
|
+
const values = useMemo(() => storedValues(value), [value]);
|
|
381
417
|
|
|
382
418
|
useEffect(() => {
|
|
383
|
-
|
|
384
|
-
}, [
|
|
419
|
+
setOptions(current => mergeOptions(current, storedReferenceOptions(source, value)));
|
|
420
|
+
}, [source, value]);
|
|
385
421
|
|
|
386
422
|
useEffect(() => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
);
|
|
390
|
-
if (missing.length === 0) return;
|
|
391
|
-
let active = true;
|
|
392
|
-
setLoading(true);
|
|
393
|
-
void resolveSource(source, operation, missing, resourceCode, labelField)
|
|
394
|
-
.then(items => {
|
|
395
|
-
if (active) setOptions(current => mergeOptions(current, items));
|
|
396
|
-
})
|
|
397
|
-
.catch(reason => {
|
|
398
|
-
if (active) setError(reason instanceof Error ? reason.message : String(reason));
|
|
399
|
-
})
|
|
400
|
-
.finally(() => {
|
|
401
|
-
if (active) setLoading(false);
|
|
402
|
-
});
|
|
403
|
-
return () => {
|
|
404
|
-
active = false;
|
|
405
|
-
};
|
|
406
|
-
}, [labelField, operation, options, resourceCode, source, values]);
|
|
423
|
+
setDraftValues(values);
|
|
424
|
+
}, [open, values]);
|
|
407
425
|
|
|
408
426
|
useEffect(() => {
|
|
409
427
|
if (disabled || !open) return;
|
|
@@ -413,7 +431,7 @@ export function MobileAuthoritativeSelector({
|
|
|
413
431
|
const timer = window.setTimeout(() => {
|
|
414
432
|
setLoading(true);
|
|
415
433
|
setError('');
|
|
416
|
-
void searchSource(source, operation, trimmed, undefined, resourceCode,
|
|
434
|
+
void searchSource(source, operation, trimmed, undefined, resourceCode, fieldCode, bindings)
|
|
417
435
|
.then(page => {
|
|
418
436
|
if (sequence !== requestSequence.current) return;
|
|
419
437
|
setOptions(current => {
|
|
@@ -432,14 +450,14 @@ export function MobileAuthoritativeSelector({
|
|
|
432
450
|
});
|
|
433
451
|
}, trimmed ? 300 : 0);
|
|
434
452
|
return () => window.clearTimeout(timer);
|
|
435
|
-
}, [disabled,
|
|
453
|
+
}, [bindings, disabled, fieldCode, keyword, open, operation, resourceCode, source, values]);
|
|
436
454
|
|
|
437
455
|
const loadNext = () => {
|
|
438
456
|
if (!cursor || loading) return;
|
|
439
457
|
const nextCursor = cursor;
|
|
440
458
|
setLoading(true);
|
|
441
459
|
setError('');
|
|
442
|
-
void searchSource(source, operation, keyword.trim(), nextCursor, resourceCode,
|
|
460
|
+
void searchSource(source, operation, keyword.trim(), nextCursor, resourceCode, fieldCode, bindings)
|
|
443
461
|
.then(page => {
|
|
444
462
|
setOptions(current => mergeOptions(current, page.items));
|
|
445
463
|
setCursor(page.nextCursor);
|
|
@@ -461,7 +479,11 @@ export function MobileAuthoritativeSelector({
|
|
|
461
479
|
const item = options.find(option => option.value === nextValue);
|
|
462
480
|
if (!item?.selectable) return;
|
|
463
481
|
if (!multiple) {
|
|
464
|
-
onChange?.(
|
|
482
|
+
onChange?.(
|
|
483
|
+
selectedValue(source, nextValue, options, false) as
|
|
484
|
+
| StoredValue
|
|
485
|
+
| undefined
|
|
486
|
+
);
|
|
465
487
|
setOpen(false);
|
|
466
488
|
return;
|
|
467
489
|
}
|
|
@@ -552,7 +574,14 @@ export function MobileAuthoritativeSelector({
|
|
|
552
574
|
<button
|
|
553
575
|
className="oxa-mobile-reference-confirm"
|
|
554
576
|
onClick={() => {
|
|
555
|
-
onChange?.(
|
|
577
|
+
onChange?.(
|
|
578
|
+
selectedValue(
|
|
579
|
+
source,
|
|
580
|
+
draftValues.length ? draftValues : undefined,
|
|
581
|
+
options,
|
|
582
|
+
true
|
|
583
|
+
) as StoredValue[] | undefined
|
|
584
|
+
);
|
|
556
585
|
setOpen(false);
|
|
557
586
|
}}
|
|
558
587
|
type="button"
|
|
@@ -570,41 +599,16 @@ export function MobileAuthoritativeSelector({
|
|
|
570
599
|
export function ResolvedValueText({
|
|
571
600
|
source,
|
|
572
601
|
value,
|
|
573
|
-
resourceCode,
|
|
574
|
-
labelField,
|
|
575
602
|
}: {
|
|
576
603
|
source: Source;
|
|
577
|
-
value?:
|
|
578
|
-
resourceCode?: string;
|
|
579
|
-
labelField?: string;
|
|
604
|
+
value?: StoredValue | StoredValue[];
|
|
580
605
|
}) {
|
|
581
|
-
const values = useMemo(
|
|
582
|
-
|
|
583
|
-
|
|
606
|
+
const values = useMemo(() => storedValues(value), [value]);
|
|
607
|
+
const items = useMemo(
|
|
608
|
+
() => storedReferenceOptions(source, value),
|
|
609
|
+
[source, value]
|
|
584
610
|
);
|
|
585
|
-
const [items, setItems] = useState<Option[]>([]);
|
|
586
|
-
const [failed, setFailed] = useState(false);
|
|
587
|
-
useEffect(() => {
|
|
588
|
-
if (values.length === 0) {
|
|
589
|
-
setItems([]);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
let active = true;
|
|
593
|
-
setFailed(false);
|
|
594
|
-
void resolveSource(source, 'update', values, resourceCode, labelField)
|
|
595
|
-
.then(next => {
|
|
596
|
-
if (active) setItems(next);
|
|
597
|
-
})
|
|
598
|
-
.catch(() => {
|
|
599
|
-
if (active) setFailed(true);
|
|
600
|
-
});
|
|
601
|
-
return () => {
|
|
602
|
-
active = false;
|
|
603
|
-
};
|
|
604
|
-
}, [labelField, resourceCode, source, values]);
|
|
605
611
|
if (values.length === 0) return <>-</>;
|
|
606
|
-
if (failed) return <Typography.Text type="danger">目录解析失败</Typography.Text>;
|
|
607
|
-
if (!items.length) return <>读取中…</>;
|
|
608
612
|
const ordered = values.map(value => items.find(item => item.value === value));
|
|
609
613
|
return (
|
|
610
614
|
<span className={`oxa-resolved-values oxa-resolved-${source}`}>
|
|
@@ -626,3 +630,32 @@ export function ResolvedValueText({
|
|
|
626
630
|
</span>
|
|
627
631
|
);
|
|
628
632
|
}
|
|
633
|
+
|
|
634
|
+
/** Resolve operational identities such as audit actors; business fields never use this path. */
|
|
635
|
+
export function DirectoryIdentityText({
|
|
636
|
+
source,
|
|
637
|
+
id,
|
|
638
|
+
}: {
|
|
639
|
+
source: DirectoryKind;
|
|
640
|
+
id: string;
|
|
641
|
+
}) {
|
|
642
|
+
const [value, setValue] = useState<UserReferenceValue | DepartmentReferenceValue>();
|
|
643
|
+
const [failed, setFailed] = useState(false);
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
let active = true;
|
|
646
|
+
setFailed(false);
|
|
647
|
+
void resolveDirectory(source, [id])
|
|
648
|
+
.then(page => {
|
|
649
|
+
if (active) setValue(page.items[0]?.snapshot);
|
|
650
|
+
})
|
|
651
|
+
.catch(() => {
|
|
652
|
+
if (active) setFailed(true);
|
|
653
|
+
});
|
|
654
|
+
return () => {
|
|
655
|
+
active = false;
|
|
656
|
+
};
|
|
657
|
+
}, [id, source]);
|
|
658
|
+
if (failed) return <Typography.Text type="secondary">{id}</Typography.Text>;
|
|
659
|
+
if (!value) return <>读取中…</>;
|
|
660
|
+
return <ResolvedValueText source={source} value={value} />;
|
|
661
|
+
}
|
|
@@ -12,7 +12,7 @@ import type { DataFilePreview } from 'openxiangda-contracts/browser';
|
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import { useParams } from 'react-router-dom';
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
downloadDataFile,
|
|
16
16
|
fetchDataFileBlob,
|
|
17
17
|
loadDataFilePreview,
|
|
18
18
|
} from './platform-client';
|
|
@@ -220,6 +220,7 @@ export function FilePreviewPage() {
|
|
|
220
220
|
const [preview, setPreview] = useState<DataFilePreview>();
|
|
221
221
|
const [blob, setBlob] = useState<Blob>();
|
|
222
222
|
const [error, setError] = useState('');
|
|
223
|
+
const [downloading, setDownloading] = useState(false);
|
|
223
224
|
useEffect(() => {
|
|
224
225
|
let active = true;
|
|
225
226
|
setPreview(undefined);
|
|
@@ -249,10 +250,21 @@ export function FilePreviewPage() {
|
|
|
249
250
|
};
|
|
250
251
|
}, [fileId, resourceCode]);
|
|
251
252
|
|
|
252
|
-
const
|
|
253
|
-
resourceCode
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
const download = async () => {
|
|
254
|
+
if (!resourceCode || !fileId) return;
|
|
255
|
+
setDownloading(true);
|
|
256
|
+
try {
|
|
257
|
+
await downloadDataFile(
|
|
258
|
+
resourceCode,
|
|
259
|
+
fileId,
|
|
260
|
+
preview?.file.name || 'attachment'
|
|
261
|
+
);
|
|
262
|
+
} catch (reason) {
|
|
263
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
264
|
+
} finally {
|
|
265
|
+
setDownloading(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
256
268
|
return (
|
|
257
269
|
<div className="oxa-file-preview-page">
|
|
258
270
|
<header className="oxa-file-preview-toolbar">
|
|
@@ -268,8 +280,12 @@ export function FilePreviewPage() {
|
|
|
268
280
|
</div>
|
|
269
281
|
</div>
|
|
270
282
|
<div>
|
|
271
|
-
{
|
|
272
|
-
<Button
|
|
283
|
+
{resourceCode && fileId && (
|
|
284
|
+
<Button
|
|
285
|
+
icon={<DownloadOutlined />}
|
|
286
|
+
loading={downloading}
|
|
287
|
+
onClick={() => void download()}
|
|
288
|
+
>
|
|
273
289
|
下载文件
|
|
274
290
|
</Button>
|
|
275
291
|
)}
|
|
@@ -285,8 +301,13 @@ export function FilePreviewPage() {
|
|
|
285
301
|
title="文件预览加载失败"
|
|
286
302
|
subTitle={error}
|
|
287
303
|
extra={
|
|
288
|
-
|
|
289
|
-
<Button
|
|
304
|
+
resourceCode && fileId ? (
|
|
305
|
+
<Button
|
|
306
|
+
icon={<DownloadOutlined />}
|
|
307
|
+
loading={downloading}
|
|
308
|
+
onClick={() => void download()}
|
|
309
|
+
type="primary"
|
|
310
|
+
>
|
|
290
311
|
下载文件
|
|
291
312
|
</Button>
|
|
292
313
|
) : undefined
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Select, Typography } from 'antd';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
RoleSubjectChoice,
|
|
5
|
+
RoleSubjectChoicePage,
|
|
6
|
+
} from 'openxiangda-contracts/browser';
|
|
7
|
+
import { loadRoleSubjects } from './platform-client';
|
|
8
|
+
|
|
9
|
+
function mergeSubjects(
|
|
10
|
+
current: RoleSubjectChoice[],
|
|
11
|
+
incoming: RoleSubjectChoice[]
|
|
12
|
+
) {
|
|
13
|
+
const merged = new Map(current.map(item => [item.subjectKey, item]));
|
|
14
|
+
for (const item of incoming) merged.set(item.subjectKey, item);
|
|
15
|
+
return [...merged.values()];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function RoleSubjectSelect({
|
|
19
|
+
disabled,
|
|
20
|
+
initialPage,
|
|
21
|
+
onChange,
|
|
22
|
+
value,
|
|
23
|
+
}: {
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
initialPage: RoleSubjectChoicePage;
|
|
26
|
+
onChange: (value: string) => void;
|
|
27
|
+
value?: string;
|
|
28
|
+
}) {
|
|
29
|
+
const [keyword, setKeyword] = useState('');
|
|
30
|
+
const [options, setOptions] = useState(initialPage.items);
|
|
31
|
+
const [cursor, setCursor] = useState(initialPage.nextCursor);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [loadError, setLoadError] = useState('');
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let active = true;
|
|
37
|
+
const timer = window.setTimeout(() => {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
setLoadError('');
|
|
40
|
+
void loadRoleSubjects({ keyword, limit: 20 })
|
|
41
|
+
.then(page => {
|
|
42
|
+
if (!active) return;
|
|
43
|
+
setOptions(current =>
|
|
44
|
+
mergeSubjects(
|
|
45
|
+
current.filter(item => item.subjectKey === value),
|
|
46
|
+
page.items
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
setCursor(page.nextCursor);
|
|
50
|
+
})
|
|
51
|
+
.catch(reason => {
|
|
52
|
+
if (!active) return;
|
|
53
|
+
setLoadError(
|
|
54
|
+
reason instanceof Error ? reason.message : '应用角色读取失败'
|
|
55
|
+
);
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (active) setLoading(false);
|
|
59
|
+
});
|
|
60
|
+
}, keyword ? 250 : 0);
|
|
61
|
+
return () => {
|
|
62
|
+
active = false;
|
|
63
|
+
window.clearTimeout(timer);
|
|
64
|
+
};
|
|
65
|
+
}, [keyword, value]);
|
|
66
|
+
|
|
67
|
+
const loadMore = async () => {
|
|
68
|
+
if (!cursor || loading) return;
|
|
69
|
+
setLoading(true);
|
|
70
|
+
setLoadError('');
|
|
71
|
+
try {
|
|
72
|
+
const page = await loadRoleSubjects({ keyword, cursor, limit: 20 });
|
|
73
|
+
setOptions(current => mergeSubjects(current, page.items));
|
|
74
|
+
setCursor(page.nextCursor);
|
|
75
|
+
} catch (reason) {
|
|
76
|
+
setLoadError(
|
|
77
|
+
reason instanceof Error ? reason.message : '更多应用角色读取失败'
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Select
|
|
86
|
+
aria-label="选择应用角色"
|
|
87
|
+
className="oxa-role-switch-select"
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
filterOption={false}
|
|
90
|
+
loading={loading}
|
|
91
|
+
notFoundContent={
|
|
92
|
+
loadError ? (
|
|
93
|
+
<Typography.Text type="danger">应用角色读取失败,请重试</Typography.Text>
|
|
94
|
+
) : undefined
|
|
95
|
+
}
|
|
96
|
+
onChange={onChange}
|
|
97
|
+
onPopupScroll={event => {
|
|
98
|
+
const target = event.currentTarget;
|
|
99
|
+
if (
|
|
100
|
+
target.scrollTop + target.clientHeight >=
|
|
101
|
+
target.scrollHeight - 12
|
|
102
|
+
) {
|
|
103
|
+
void loadMore();
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
onSearch={setKeyword}
|
|
107
|
+
optionRender={option => {
|
|
108
|
+
const subject = options.find(item => item.subjectKey === option.value);
|
|
109
|
+
return (
|
|
110
|
+
<div className="oxa-role-switch-option">
|
|
111
|
+
<strong>{subject?.role.name || option.label}</strong>
|
|
112
|
+
{subject?.role.description && (
|
|
113
|
+
<span>{subject.role.description}</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}}
|
|
118
|
+
options={options.map(subject => ({
|
|
119
|
+
label: subject.role.name,
|
|
120
|
+
value: subject.subjectKey,
|
|
121
|
+
}))}
|
|
122
|
+
placeholder="选择当前应用角色"
|
|
123
|
+
showSearch
|
|
124
|
+
status={loadError ? 'error' : undefined}
|
|
125
|
+
value={value}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|