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,661 +0,0 @@
1
- import {
2
- ApartmentOutlined,
3
- DatabaseOutlined,
4
- UserOutlined,
5
- } from '@ant-design/icons';
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';
13
- import { useEffect, useMemo, useRef, useState } from 'react';
14
- import {
15
- resolveDirectory,
16
- searchDirectory,
17
- searchResource,
18
- type DirectoryKind,
19
- } from './platform-client';
20
-
21
- type Source = DirectoryKind | 'resource';
22
- type Option = {
23
- value: string;
24
- label: string;
25
- selectable: boolean;
26
- description?: string;
27
- path?: string[];
28
- source: Source;
29
- reference?: StoredValue;
30
- };
31
-
32
- type StoredValue =
33
- | UserReferenceValue
34
- | DepartmentReferenceValue
35
- | ResourceReferenceValue;
36
-
37
- function mergeOptions(current: Option[], incoming: Option[]) {
38
- const merged = new Map(current.map(option => [option.value, option]));
39
- for (const option of incoming) merged.set(option.value, option);
40
- return [...merged.values()];
41
- }
42
-
43
- function directoryOption(source: DirectoryKind, item: DirectoryEntry): Option {
44
- return {
45
- source,
46
- value: item.id,
47
- label: item.label,
48
- selectable: item.selectable !== false,
49
- ...(item.description ? { description: item.description } : {}),
50
- ...(item.path?.length
51
- ? { path: item.path.map(entry => entry.label) }
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,
65
- };
66
- }
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
-
107
- async function searchSource(
108
- source: Source,
109
- operation: 'create' | 'update',
110
- keyword: string,
111
- cursor?: string,
112
- resourceCode?: string,
113
- fieldCode?: string,
114
- bindings?: Record<string, unknown>,
115
- ) {
116
- if (source === 'resource') {
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
- };
128
- }
129
- const page = await searchDirectory(source, { keyword, cursor });
130
- return {
131
- items: page.items.map(item => directoryOption(source, item)),
132
- nextCursor: page.nextCursor,
133
- };
134
- }
135
-
136
- function optionIcon(option: Pick<Option, 'source' | 'label'>) {
137
- if (option.source === 'user') {
138
- return (
139
- <Avatar className="oxa-selector-avatar" size={28}>
140
- {option.label.slice(0, 1) || <UserOutlined />}
141
- </Avatar>
142
- );
143
- }
144
- return option.source === 'resource' ? (
145
- <DatabaseOutlined className="oxa-selector-icon" />
146
- ) : (
147
- <ApartmentOutlined className="oxa-selector-icon" />
148
- );
149
- }
150
-
151
- function optionDescription(option: Option) {
152
- if (option.path?.length) return option.path.join(' / ');
153
- return option.description || '';
154
- }
155
-
156
- export type AuthoritativeSelectorProps = {
157
- value?: StoredValue | StoredValue[];
158
- onChange?: (value: StoredValue | StoredValue[] | undefined) => void;
159
- source: Source;
160
- operation: 'create' | 'update';
161
- multiple?: boolean;
162
- disabled?: boolean;
163
- id?: string;
164
- placeholder: string;
165
- /** Host resource and field; the platform resolves the target source declaration. */
166
- resourceCode?: string;
167
- fieldCode?: string;
168
- bindings?: Record<string, unknown>;
169
- };
170
-
171
- export function AuthoritativeSelector({
172
- value,
173
- onChange,
174
- source,
175
- operation,
176
- multiple = false,
177
- disabled = false,
178
- id,
179
- placeholder,
180
- resourceCode,
181
- fieldCode,
182
- bindings,
183
- }: AuthoritativeSelectorProps) {
184
- const [options, setOptions] = useState<Option[]>(() =>
185
- storedReferenceOptions(source, value)
186
- );
187
- const [keyword, setKeyword] = useState('');
188
- const [cursor, setCursor] = useState<string | null>(null);
189
- const [loading, setLoading] = useState(false);
190
- const [error, setError] = useState('');
191
- const [open, setOpen] = useState(false);
192
- const requestSequence = useRef(0);
193
- const values = useMemo(() => storedValues(value), [value]);
194
-
195
- useEffect(() => {
196
- setOptions(current => mergeOptions(current, storedReferenceOptions(source, value)));
197
- }, [source, value]);
198
-
199
- useEffect(() => {
200
- if (disabled) return;
201
- if (keyword.trim().length < 2) {
202
- setCursor(null);
203
- return;
204
- }
205
- const sequence = ++requestSequence.current;
206
- const timer = window.setTimeout(() => {
207
- setLoading(true);
208
- setError('');
209
- void searchSource(source, operation, keyword, undefined, resourceCode, fieldCode, bindings)
210
- .then(page => {
211
- if (sequence !== requestSequence.current) return;
212
- setOptions(current => {
213
- const selected = current.filter(option =>
214
- values.includes(option.value)
215
- );
216
- return mergeOptions(selected, page.items);
217
- });
218
- setCursor(page.nextCursor);
219
- })
220
- .catch(reason => {
221
- if (sequence === requestSequence.current)
222
- setError(
223
- reason instanceof Error ? reason.message : String(reason)
224
- );
225
- })
226
- .finally(() => {
227
- if (sequence === requestSequence.current) setLoading(false);
228
- });
229
- }, 300);
230
- return () => window.clearTimeout(timer);
231
- }, [bindings, disabled, fieldCode, keyword, operation, resourceCode, source, values]);
232
-
233
- useEffect(() => {
234
- if (disabled || !open || keyword.trim().length >= 2) return;
235
- const sequence = ++requestSequence.current;
236
- setLoading(true);
237
- setError('');
238
- void searchSource(source, operation, '', undefined, resourceCode, fieldCode, bindings)
239
- .then(page => {
240
- if (sequence !== requestSequence.current) return;
241
- setOptions(current => {
242
- const selected = current.filter(option => values.includes(option.value));
243
- return mergeOptions(selected, page.items);
244
- });
245
- setCursor(page.nextCursor);
246
- })
247
- .catch(reason => {
248
- if (sequence === requestSequence.current) {
249
- setError(reason instanceof Error ? reason.message : String(reason));
250
- }
251
- })
252
- .finally(() => {
253
- if (sequence === requestSequence.current) setLoading(false);
254
- });
255
- }, [bindings, disabled, fieldCode, keyword, open, operation, resourceCode, source, values]);
256
-
257
- const loadNext = () => {
258
- if (!cursor || loading) return;
259
- const nextCursor = cursor;
260
- setLoading(true);
261
- setError('');
262
- void searchSource(source, operation, keyword, nextCursor, resourceCode, fieldCode, bindings)
263
- .then(page => {
264
- setOptions(current => mergeOptions(current, page.items));
265
- setCursor(page.nextCursor);
266
- })
267
- .catch(reason =>
268
- setError(reason instanceof Error ? reason.message : String(reason))
269
- )
270
- .finally(() => setLoading(false));
271
- };
272
-
273
- return (
274
- <Select
275
- allowClear
276
- className={`oxa-authoritative-selector oxa-selector-${source}`}
277
- disabled={disabled}
278
- id={id}
279
- labelRender={({ label, value: selectedValue }) => {
280
- const selected = options.find(
281
- option => option.value === String(selectedValue)
282
- );
283
- return selected?.label || label || '读取中…';
284
- }}
285
- loading={loading}
286
- maxTagCount="responsive"
287
- mode={multiple ? 'multiple' : undefined}
288
- notFoundContent={
289
- loading ? (
290
- <Spin size="small" />
291
- ) : error ? (
292
- <Typography.Text type="danger">{error}</Typography.Text>
293
- ) : keyword.trim().length < 2 ? (
294
- '暂无可选数据,可输入至少 2 个字符搜索'
295
- ) : (
296
- '无可选数据'
297
- )
298
- }
299
- onChange={next => {
300
- onChange?.(
301
- selectedValue(
302
- source,
303
- (next || undefined) as string | string[] | undefined,
304
- options,
305
- multiple
306
- )
307
- );
308
- if (!multiple) setOpen(false);
309
- }}
310
- onOpenChange={next => {
311
- setOpen(next);
312
- if (!next) setKeyword('');
313
- }}
314
- onPopupScroll={event => {
315
- const element = event.currentTarget;
316
- if (
317
- element.scrollTop + element.clientHeight >=
318
- element.scrollHeight - 16
319
- )
320
- loadNext();
321
- }}
322
- open={open}
323
- optionRender={option => {
324
- const item = options.find(
325
- current => current.value === String(option.value)
326
- );
327
- if (!item) return option.label;
328
- const description = optionDescription(item);
329
- return (
330
- <Space className="oxa-selector-option" size={10}>
331
- {optionIcon(item)}
332
- <span className="oxa-selector-option-copy">
333
- <strong>{item.label}</strong>
334
- {description && <small>{description}</small>}
335
- </span>
336
- </Space>
337
- );
338
- }}
339
- options={options.map(option => ({
340
- label: option.label,
341
- value: option.value,
342
- disabled: !option.selectable,
343
- }))}
344
- placeholder={placeholder}
345
- prefix={
346
- source === 'user' ? (
347
- <UserOutlined />
348
- ) : source === 'resource' ? (
349
- <DatabaseOutlined />
350
- ) : (
351
- <ApartmentOutlined />
352
- )
353
- }
354
- showSearch={{
355
- filterOption: false,
356
- onSearch: next => {
357
- setKeyword(next);
358
- setOpen(true);
359
- },
360
- }}
361
- status={error ? 'error' : undefined}
362
- tagRender={({ label, value: selectedValue, closable, onClose }) => {
363
- const selected = options.find(
364
- option => option.value === String(selectedValue)
365
- );
366
- return (
367
- <Tag
368
- className="oxa-selector-tag"
369
- closable={closable}
370
- onClose={onClose}
371
- onMouseDown={event => {
372
- event.preventDefault();
373
- event.stopPropagation();
374
- }}
375
- >
376
- {selected && optionIcon(selected)}
377
- <span>{selected?.label || label || '读取中…'}</span>
378
- {selected?.description && <small>{selected.description}</small>}
379
- </Tag>
380
- );
381
- }}
382
- value={multiple ? values : values[0]}
383
- />
384
- );
385
- }
386
-
387
- /**
388
- * Mobile reference picker. It deliberately avoids Ant Design Select so that
389
- * resource references have a touch-friendly, full-width interaction on
390
- * phones. The search and resolve calls are the same authoritative platform
391
- * APIs used by the desktop selector.
392
- */
393
- export function MobileAuthoritativeSelector({
394
- value,
395
- onChange,
396
- source,
397
- operation,
398
- multiple = false,
399
- disabled = false,
400
- id,
401
- placeholder,
402
- resourceCode,
403
- fieldCode,
404
- bindings,
405
- }: AuthoritativeSelectorProps) {
406
- const [options, setOptions] = useState<Option[]>(() =>
407
- storedReferenceOptions(source, value)
408
- );
409
- const [keyword, setKeyword] = useState('');
410
- const [cursor, setCursor] = useState<string | null>(null);
411
- const [loading, setLoading] = useState(false);
412
- const [error, setError] = useState('');
413
- const [open, setOpen] = useState(false);
414
- const [draftValues, setDraftValues] = useState<string[]>([]);
415
- const requestSequence = useRef(0);
416
- const values = useMemo(() => storedValues(value), [value]);
417
-
418
- useEffect(() => {
419
- setOptions(current => mergeOptions(current, storedReferenceOptions(source, value)));
420
- }, [source, value]);
421
-
422
- useEffect(() => {
423
- setDraftValues(values);
424
- }, [open, values]);
425
-
426
- useEffect(() => {
427
- if (disabled || !open) return;
428
- const trimmed = keyword.trim();
429
- if (trimmed.length > 0 && trimmed.length < 2) return;
430
- const sequence = ++requestSequence.current;
431
- const timer = window.setTimeout(() => {
432
- setLoading(true);
433
- setError('');
434
- void searchSource(source, operation, trimmed, undefined, resourceCode, fieldCode, bindings)
435
- .then(page => {
436
- if (sequence !== requestSequence.current) return;
437
- setOptions(current => {
438
- const selected = current.filter(option => values.includes(option.value));
439
- return mergeOptions(selected, page.items);
440
- });
441
- setCursor(page.nextCursor);
442
- })
443
- .catch(reason => {
444
- if (sequence === requestSequence.current) {
445
- setError(reason instanceof Error ? reason.message : String(reason));
446
- }
447
- })
448
- .finally(() => {
449
- if (sequence === requestSequence.current) setLoading(false);
450
- });
451
- }, trimmed ? 300 : 0);
452
- return () => window.clearTimeout(timer);
453
- }, [bindings, disabled, fieldCode, keyword, open, operation, resourceCode, source, values]);
454
-
455
- const loadNext = () => {
456
- if (!cursor || loading) return;
457
- const nextCursor = cursor;
458
- setLoading(true);
459
- setError('');
460
- void searchSource(source, operation, keyword.trim(), nextCursor, resourceCode, fieldCode, bindings)
461
- .then(page => {
462
- setOptions(current => mergeOptions(current, page.items));
463
- setCursor(page.nextCursor);
464
- })
465
- .catch(reason => setError(reason instanceof Error ? reason.message : String(reason)))
466
- .finally(() => setLoading(false));
467
- };
468
-
469
- const selectedItems = values
470
- .map(selected => options.find(option => option.value === selected))
471
- .filter((item): item is Option => Boolean(item));
472
- const selectedLabel = selectedItems.length
473
- ? selectedItems.map(item => item.label).join('、')
474
- : values.length
475
- ? (loading ? '读取中…' : values.join('、'))
476
- : `请选择${placeholder}`;
477
-
478
- const toggleValue = (nextValue: string) => {
479
- const item = options.find(option => option.value === nextValue);
480
- if (!item?.selectable) return;
481
- if (!multiple) {
482
- onChange?.(
483
- selectedValue(source, nextValue, options, false) as
484
- | StoredValue
485
- | undefined
486
- );
487
- setOpen(false);
488
- return;
489
- }
490
- setDraftValues(current =>
491
- current.includes(nextValue)
492
- ? current.filter(itemValue => itemValue !== nextValue)
493
- : [...current, nextValue]
494
- );
495
- };
496
-
497
- return (
498
- <>
499
- <button
500
- aria-haspopup="dialog"
501
- className="oxa-mobile-reference-trigger"
502
- disabled={disabled}
503
- id={id}
504
- onClick={() => {
505
- setDraftValues(values);
506
- setKeyword('');
507
- setOpen(true);
508
- }}
509
- type="button"
510
- >
511
- <span className={values.length ? '' : 'oxa-mobile-reference-placeholder'}>
512
- {selectedLabel}
513
- </span>
514
- <span aria-hidden="true">⌄</span>
515
- </button>
516
- {open && (
517
- <div
518
- aria-label={placeholder}
519
- aria-modal="true"
520
- className="oxa-mobile-reference-backdrop"
521
- onClick={() => setOpen(false)}
522
- role="dialog"
523
- >
524
- <div className="oxa-mobile-reference-sheet" onClick={event => event.stopPropagation()}>
525
- <div className="oxa-mobile-reference-header">
526
- <strong>{placeholder}</strong>
527
- <button onClick={() => setOpen(false)} type="button">关闭</button>
528
- </div>
529
- <input
530
- autoFocus
531
- className="oxa-mobile-input oxa-mobile-reference-search"
532
- onChange={event => setKeyword(event.target.value)}
533
- placeholder={`搜索${placeholder}`}
534
- type="search"
535
- value={keyword}
536
- />
537
- {error && <div className="oxa-mobile-reference-error">{error}</div>}
538
- {loading && <div className="oxa-mobile-reference-loading">加载中…</div>}
539
- {!loading && !error && options.length === 0 && (
540
- <div className="oxa-mobile-reference-empty">
541
- {keyword.trim().length < 2 ? '请输入至少 2 个字符搜索' : '暂无可选数据'}
542
- </div>
543
- )}
544
- <div aria-multiselectable={multiple} className="oxa-mobile-reference-options" role="listbox">
545
- {options.map(option => {
546
- const selected = (multiple ? draftValues : values).includes(option.value);
547
- const description = optionDescription(option);
548
- return (
549
- <button
550
- aria-selected={selected}
551
- className={`oxa-mobile-reference-option${selected ? ' is-selected' : ''}`}
552
- disabled={!option.selectable}
553
- key={option.value}
554
- onClick={() => toggleValue(option.value)}
555
- role="option"
556
- type="button"
557
- >
558
- {optionIcon(option)}
559
- <span className="oxa-mobile-reference-option-copy">
560
- <strong>{option.label}</strong>
561
- {description && <small>{description}</small>}
562
- </span>
563
- {selected && <span aria-hidden="true">✓</span>}
564
- </button>
565
- );
566
- })}
567
- </div>
568
- {cursor && (
569
- <button className="oxa-mobile-reference-more" onClick={loadNext} type="button">
570
- {loading ? '加载中…' : '加载更多'}
571
- </button>
572
- )}
573
- {multiple && (
574
- <button
575
- className="oxa-mobile-reference-confirm"
576
- onClick={() => {
577
- onChange?.(
578
- selectedValue(
579
- source,
580
- draftValues.length ? draftValues : undefined,
581
- options,
582
- true
583
- ) as StoredValue[] | undefined
584
- );
585
- setOpen(false);
586
- }}
587
- type="button"
588
- >
589
- 确定({draftValues.length})
590
- </button>
591
- )}
592
- </div>
593
- </div>
594
- )}
595
- </>
596
- );
597
- }
598
-
599
- export function ResolvedValueText({
600
- source,
601
- value,
602
- }: {
603
- source: Source;
604
- value?: StoredValue | StoredValue[];
605
- }) {
606
- const values = useMemo(() => storedValues(value), [value]);
607
- const items = useMemo(
608
- () => storedReferenceOptions(source, value),
609
- [source, value]
610
- );
611
- if (values.length === 0) return <>-</>;
612
- const ordered = values.map(value => items.find(item => item.value === value));
613
- return (
614
- <span className={`oxa-resolved-values oxa-resolved-${source}`}>
615
- {ordered.map((item, index) =>
616
- item ? (
617
- <span className="oxa-resolved-item" key={item.value}>
618
- {optionIcon(item)}
619
- <span>
620
- <strong>{item.label}</strong>
621
- {optionDescription(item) && <small>{optionDescription(item)}</small>}
622
- </span>
623
- </span>
624
- ) : (
625
- <Typography.Text key={values[index]} type="secondary">
626
- 目录项不可用
627
- </Typography.Text>
628
- )
629
- )}
630
- </span>
631
- );
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
- }