openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,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
- }