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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { DatePicker, TimePicker, Typography } from 'antd';
|
|
2
|
+
import type { DataFieldSurface } from 'openxiangda-contracts/browser';
|
|
3
|
+
import { fieldValueForData, fieldValueForForm } from './field-form-codec';
|
|
4
|
+
|
|
5
|
+
export function DateTimeField({
|
|
6
|
+
field,
|
|
7
|
+
disabled,
|
|
8
|
+
mobile = false,
|
|
9
|
+
}: {
|
|
10
|
+
field: DataFieldSurface;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
mobile?: boolean;
|
|
13
|
+
}) {
|
|
14
|
+
const className = mobile ? 'oxa-mobile-date-time-field' : undefined;
|
|
15
|
+
switch (field.type) {
|
|
16
|
+
case 'time': {
|
|
17
|
+
const format = field.timePrecision === 'minute' ? 'HH:mm' : 'HH:mm:ss';
|
|
18
|
+
return (
|
|
19
|
+
<TimePicker
|
|
20
|
+
className={className}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
format={format}
|
|
23
|
+
needConfirm={false}
|
|
24
|
+
placeholder={`请选择${field.label}`}
|
|
25
|
+
showSecond={field.timePrecision !== 'minute'}
|
|
26
|
+
style={{ width: '100%' }}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
case 'datetime':
|
|
31
|
+
return (
|
|
32
|
+
<DatePicker
|
|
33
|
+
className={className}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
format="YYYY-MM-DD HH:mm:ss"
|
|
36
|
+
placeholder={`请选择${field.label}`}
|
|
37
|
+
showTime={{ format: 'HH:mm:ss' }}
|
|
38
|
+
style={{ width: '100%' }}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
case 'date-range':
|
|
42
|
+
return (
|
|
43
|
+
<DatePicker.RangePicker
|
|
44
|
+
className={className}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
format="YYYY-MM-DD"
|
|
47
|
+
placeholder={[`开始${field.label}`, `结束${field.label}`]}
|
|
48
|
+
style={{ width: '100%' }}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
case 'datetime-range':
|
|
52
|
+
return (
|
|
53
|
+
<DatePicker.RangePicker
|
|
54
|
+
className={className}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
format="YYYY-MM-DD HH:mm:ss"
|
|
57
|
+
placeholder={[`开始${field.label}`, `结束${field.label}`]}
|
|
58
|
+
showTime={{ format: 'HH:mm:ss' }}
|
|
59
|
+
style={{ width: '100%' }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
case 'date':
|
|
63
|
+
default:
|
|
64
|
+
return (
|
|
65
|
+
<DatePicker
|
|
66
|
+
className={className}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
format="YYYY-MM-DD"
|
|
69
|
+
placeholder={`请选择${field.label}`}
|
|
70
|
+
style={{ width: '100%' }}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function DateTimeValueDisplay({
|
|
77
|
+
field,
|
|
78
|
+
value,
|
|
79
|
+
}: {
|
|
80
|
+
field: DataFieldSurface;
|
|
81
|
+
value: unknown;
|
|
82
|
+
}) {
|
|
83
|
+
if (field.type === 'date-range' || field.type === 'datetime-range') {
|
|
84
|
+
const range = value as { start?: unknown; end?: unknown };
|
|
85
|
+
return <>{range?.start && range?.end ? `${range.start} 至 ${range.end}` : '-'}</>;
|
|
86
|
+
}
|
|
87
|
+
if (field.type === 'time') {
|
|
88
|
+
const time = String(value);
|
|
89
|
+
return <>{field.timePrecision === 'minute' ? time.slice(0, 5) : time}</>;
|
|
90
|
+
}
|
|
91
|
+
if (field.type === 'datetime') {
|
|
92
|
+
const parsed = new Date(String(value));
|
|
93
|
+
return <>{Number.isNaN(parsed.getTime()) ? String(value) : parsed.toLocaleString('zh-CN', { hour12: false })}</>;
|
|
94
|
+
}
|
|
95
|
+
return <Typography.Text>{String(value)}</Typography.Text>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function DateTimeFilter({
|
|
99
|
+
field,
|
|
100
|
+
value,
|
|
101
|
+
onChange,
|
|
102
|
+
}: {
|
|
103
|
+
field: DataFieldSurface;
|
|
104
|
+
value: unknown;
|
|
105
|
+
onChange: (value: unknown) => void;
|
|
106
|
+
}) {
|
|
107
|
+
const formValue = fieldValueForForm(field, value) as never;
|
|
108
|
+
const emit = (next: unknown) => onChange(
|
|
109
|
+
next === null ? undefined : fieldValueForData(field, next)
|
|
110
|
+
);
|
|
111
|
+
if (field.type === 'time') {
|
|
112
|
+
const format = field.timePrecision === 'minute' ? 'HH:mm' : 'HH:mm:ss';
|
|
113
|
+
return (
|
|
114
|
+
<TimePicker
|
|
115
|
+
allowClear
|
|
116
|
+
format={format}
|
|
117
|
+
onChange={emit}
|
|
118
|
+
placeholder={`筛选${field.label}`}
|
|
119
|
+
showSecond={field.timePrecision !== 'minute'}
|
|
120
|
+
value={formValue}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (field.type === 'date-range' || field.type === 'datetime-range') {
|
|
125
|
+
const datetime = field.type === 'datetime-range';
|
|
126
|
+
return (
|
|
127
|
+
<DatePicker.RangePicker
|
|
128
|
+
allowClear
|
|
129
|
+
format={datetime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'}
|
|
130
|
+
onChange={emit}
|
|
131
|
+
placeholder={[`开始${field.label}`, `结束${field.label}`]}
|
|
132
|
+
showTime={datetime ? { format: 'HH:mm:ss' } : false}
|
|
133
|
+
value={formValue}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const datetime = field.type === 'datetime';
|
|
138
|
+
return (
|
|
139
|
+
<DatePicker
|
|
140
|
+
allowClear
|
|
141
|
+
format={datetime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'}
|
|
142
|
+
onChange={emit}
|
|
143
|
+
placeholder={`筛选${field.label}`}
|
|
144
|
+
showTime={datetime ? { format: 'HH:mm:ss' } : false}
|
|
145
|
+
value={formValue}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { CheckCircleOutlined, FormatPainterOutlined } from '@ant-design/icons';
|
|
2
|
+
import { Alert, Button, Input, Space } from 'antd';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
function stringify(value: unknown) {
|
|
6
|
+
if (value === undefined) return '';
|
|
7
|
+
return JSON.stringify(value, null, 2);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseJsonEditorValue(value: string) {
|
|
11
|
+
const text = value.trim();
|
|
12
|
+
return text ? JSON.parse(text) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function JsonValueDisplay({ value }: { value: unknown }) {
|
|
16
|
+
return <pre className="oxa-json-value">{stringify(value)}</pre>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function JsonField({
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
disabled = false,
|
|
23
|
+
mobile = false,
|
|
24
|
+
}: {
|
|
25
|
+
value?: unknown;
|
|
26
|
+
onChange?: (value: unknown) => void;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
mobile?: boolean;
|
|
29
|
+
}) {
|
|
30
|
+
const [draft, setDraft] = useState(() => stringify(value));
|
|
31
|
+
const [error, setError] = useState('');
|
|
32
|
+
|
|
33
|
+
useEffect(() => setDraft(stringify(value)), [value]);
|
|
34
|
+
|
|
35
|
+
const validate = (next: string) => {
|
|
36
|
+
setDraft(next);
|
|
37
|
+
try {
|
|
38
|
+
const parsed = parseJsonEditorValue(next);
|
|
39
|
+
setError('');
|
|
40
|
+
onChange?.(parsed);
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch (reason) {
|
|
43
|
+
setError(reason instanceof Error ? reason.message : 'JSON 格式无效');
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={`oxa-json-field${mobile ? ' oxa-mobile-json-field' : ''}`}>
|
|
50
|
+
<Input.TextArea
|
|
51
|
+
autoSize={{ minRows: mobile ? 8 : 6, maxRows: 18 }}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
onChange={event => validate(event.target.value)}
|
|
54
|
+
placeholder='例如:{"enabled": true}'
|
|
55
|
+
spellCheck={false}
|
|
56
|
+
value={draft}
|
|
57
|
+
/>
|
|
58
|
+
<Space>
|
|
59
|
+
<Button
|
|
60
|
+
disabled={disabled || Boolean(error) || !draft.trim()}
|
|
61
|
+
icon={<FormatPainterOutlined />}
|
|
62
|
+
onClick={() => {
|
|
63
|
+
const parsed = validate(draft);
|
|
64
|
+
if (parsed !== undefined) setDraft(stringify(parsed));
|
|
65
|
+
}}
|
|
66
|
+
size="small"
|
|
67
|
+
>
|
|
68
|
+
格式化
|
|
69
|
+
</Button>
|
|
70
|
+
{!error && draft.trim() && (
|
|
71
|
+
<span className="oxa-json-valid"><CheckCircleOutlined /> 格式有效</span>
|
|
72
|
+
)}
|
|
73
|
+
</Space>
|
|
74
|
+
{error && <Alert showIcon title={`JSON 格式无效:${error}`} type="error" />}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AimOutlined,
|
|
3
|
+
DeleteOutlined,
|
|
4
|
+
EnvironmentOutlined,
|
|
5
|
+
} from '@ant-design/icons';
|
|
6
|
+
import { Alert, Button, Space, Tooltip, Typography } from 'antd';
|
|
7
|
+
import type { StableLocationValue } from 'openxiangda-contracts/browser';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
browserLocationValue,
|
|
11
|
+
dingTalkLocationValue,
|
|
12
|
+
type DingTalkCoordinateSnapshot,
|
|
13
|
+
} from './location-value';
|
|
14
|
+
|
|
15
|
+
interface DingTalkGeolocationApi {
|
|
16
|
+
get(options: {
|
|
17
|
+
targetAccuracy: number;
|
|
18
|
+
coordinate: 0;
|
|
19
|
+
withReGeocode: true;
|
|
20
|
+
useCache: false;
|
|
21
|
+
onSuccess: (result: DingTalkCoordinateSnapshot) => void;
|
|
22
|
+
onFail: (error: unknown) => void;
|
|
23
|
+
}): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CaptureSource = StableLocationValue['source'];
|
|
27
|
+
|
|
28
|
+
export function LocationValueDisplay({ value }: { value: StableLocationValue }) {
|
|
29
|
+
return (
|
|
30
|
+
<span className="oxa-location-value oxa-location-value-readonly">
|
|
31
|
+
<Typography.Text strong>
|
|
32
|
+
{value.longitude.toFixed(6)}, {value.latitude.toFixed(6)}
|
|
33
|
+
</Typography.Text>
|
|
34
|
+
<Typography.Text type="secondary">
|
|
35
|
+
{value.source === 'dingTalk' ? '钉钉定位' : '浏览器定位'}
|
|
36
|
+
{value.accuracy === undefined ? '' : ` · 精度 ${Math.round(value.accuracy)} 米`}
|
|
37
|
+
</Typography.Text>
|
|
38
|
+
{(value.name || value.address) && (
|
|
39
|
+
<Typography.Text>{value.name || value.address}</Typography.Text>
|
|
40
|
+
)}
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dingTalkGeolocation() {
|
|
46
|
+
if (typeof window === 'undefined') return undefined;
|
|
47
|
+
return (
|
|
48
|
+
window as unknown as {
|
|
49
|
+
dd?: { device?: { geolocation?: DingTalkGeolocationApi } };
|
|
50
|
+
}
|
|
51
|
+
).dd?.device?.geolocation;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function browserCapture() {
|
|
55
|
+
return new Promise<StableLocationValue>((resolve, reject) => {
|
|
56
|
+
if (!navigator.geolocation) {
|
|
57
|
+
reject(new Error('当前浏览器不支持定位'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
navigator.geolocation.getCurrentPosition(
|
|
61
|
+
position => resolve(browserLocationValue({
|
|
62
|
+
longitude: position.coords.longitude,
|
|
63
|
+
latitude: position.coords.latitude,
|
|
64
|
+
accuracy: position.coords.accuracy,
|
|
65
|
+
timestamp: position.timestamp,
|
|
66
|
+
})),
|
|
67
|
+
error => reject(new Error(error.message || '浏览器定位失败')),
|
|
68
|
+
{ enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 }
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dingTalkCapture() {
|
|
74
|
+
return new Promise<StableLocationValue>((resolve, reject) => {
|
|
75
|
+
const geolocation = dingTalkGeolocation();
|
|
76
|
+
if (!geolocation) {
|
|
77
|
+
reject(new Error('当前环境未提供钉钉定位能力'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
geolocation.get({
|
|
81
|
+
targetAccuracy: 20,
|
|
82
|
+
coordinate: 0,
|
|
83
|
+
withReGeocode: true,
|
|
84
|
+
useCache: false,
|
|
85
|
+
onSuccess: result => {
|
|
86
|
+
try {
|
|
87
|
+
resolve(dingTalkLocationValue(result));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
reject(error);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
onFail: error => reject(new Error(
|
|
93
|
+
typeof error === 'object' && error && 'errorMessage' in error
|
|
94
|
+
? String(error.errorMessage)
|
|
95
|
+
: '钉钉定位失败'
|
|
96
|
+
)),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function LocationField({
|
|
102
|
+
value,
|
|
103
|
+
onChange,
|
|
104
|
+
disabled,
|
|
105
|
+
mobile = false,
|
|
106
|
+
}: {
|
|
107
|
+
value?: StableLocationValue;
|
|
108
|
+
onChange?: (value: StableLocationValue | undefined) => void;
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
mobile?: boolean;
|
|
111
|
+
}) {
|
|
112
|
+
const [capturing, setCapturing] = useState<CaptureSource>();
|
|
113
|
+
const [error, setError] = useState('');
|
|
114
|
+
const hasDingTalk = Boolean(dingTalkGeolocation());
|
|
115
|
+
|
|
116
|
+
const capture = async (source: CaptureSource) => {
|
|
117
|
+
setCapturing(source);
|
|
118
|
+
setError('');
|
|
119
|
+
try {
|
|
120
|
+
onChange?.(await (source === 'dingTalk' ? dingTalkCapture() : browserCapture()));
|
|
121
|
+
} catch (captureError) {
|
|
122
|
+
setError(captureError instanceof Error ? captureError.message : String(captureError));
|
|
123
|
+
} finally {
|
|
124
|
+
setCapturing(undefined);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className={`oxa-location-field${mobile ? ' oxa-mobile-location-field' : ''}`}>
|
|
130
|
+
<Space wrap>
|
|
131
|
+
<Tooltip title={hasDingTalk ? '使用钉钉定位' : '仅在钉钉客户端内可用'}>
|
|
132
|
+
<Button
|
|
133
|
+
disabled={disabled || !hasDingTalk}
|
|
134
|
+
icon={<EnvironmentOutlined />}
|
|
135
|
+
loading={capturing === 'dingTalk'}
|
|
136
|
+
onClick={() => void capture('dingTalk')}
|
|
137
|
+
>
|
|
138
|
+
钉钉定位
|
|
139
|
+
</Button>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
<Button
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
icon={<AimOutlined />}
|
|
144
|
+
loading={capturing === 'browser'}
|
|
145
|
+
onClick={() => void capture('browser')}
|
|
146
|
+
>
|
|
147
|
+
浏览器定位
|
|
148
|
+
</Button>
|
|
149
|
+
{value && (
|
|
150
|
+
<Tooltip title="清除定位">
|
|
151
|
+
<Button
|
|
152
|
+
aria-label="清除定位"
|
|
153
|
+
disabled={disabled || Boolean(capturing)}
|
|
154
|
+
icon={<DeleteOutlined />}
|
|
155
|
+
onClick={() => onChange?.(undefined)}
|
|
156
|
+
/>
|
|
157
|
+
</Tooltip>
|
|
158
|
+
)}
|
|
159
|
+
</Space>
|
|
160
|
+
{error && <Alert showIcon title={error} type="error" />}
|
|
161
|
+
{value && <LocationValueDisplay value={value} />}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
Typography,
|
|
21
21
|
type TreeDataNode,
|
|
22
22
|
} from 'antd';
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
DirectoryEntry,
|
|
25
|
+
} from 'openxiangda-contracts/browser';
|
|
24
26
|
import {
|
|
25
27
|
useCallback,
|
|
26
28
|
useEffect,
|
|
@@ -32,17 +34,18 @@ import {
|
|
|
32
34
|
import {
|
|
33
35
|
browseDepartmentTree,
|
|
34
36
|
browseDepartmentUsers,
|
|
35
|
-
resolveDirectory,
|
|
36
37
|
searchDirectory,
|
|
37
38
|
type DirectoryKind,
|
|
38
39
|
} from '../../platform-client';
|
|
40
|
+
import {
|
|
41
|
+
directoryEntryFromStored,
|
|
42
|
+
directoryStoredValueFromEntries,
|
|
43
|
+
directoryStoredValues,
|
|
44
|
+
type DirectoryStoredValue,
|
|
45
|
+
} from './directory-value';
|
|
39
46
|
|
|
40
47
|
type DepartmentNode = DirectoryEntry & { children?: DepartmentNode[] };
|
|
41
48
|
|
|
42
|
-
function selectedValues(value?: string | string[]) {
|
|
43
|
-
return Array.isArray(value) ? value : value ? [value] : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
49
|
function replaceChildren(
|
|
47
50
|
nodes: DepartmentNode[],
|
|
48
51
|
parentId: string,
|
|
@@ -82,8 +85,8 @@ export function PlatformDirectoryPicker({
|
|
|
82
85
|
id,
|
|
83
86
|
}: {
|
|
84
87
|
kind: DirectoryKind;
|
|
85
|
-
value?:
|
|
86
|
-
onChange?: (value:
|
|
88
|
+
value?: DirectoryStoredValue | DirectoryStoredValue[];
|
|
89
|
+
onChange?: (value: DirectoryStoredValue | DirectoryStoredValue[] | undefined) => void;
|
|
87
90
|
multiple?: boolean;
|
|
88
91
|
disabled?: boolean;
|
|
89
92
|
placeholder: string;
|
|
@@ -91,48 +94,24 @@ export function PlatformDirectoryPicker({
|
|
|
91
94
|
}) {
|
|
92
95
|
const screens = Grid.useBreakpoint();
|
|
93
96
|
const mobile = !screens.md;
|
|
94
|
-
const values = useMemo(() =>
|
|
97
|
+
const values = useMemo(() => directoryStoredValues(value), [value]);
|
|
95
98
|
const [open, setOpen] = useState(false);
|
|
96
|
-
const [selected, setSelected] = useState<DirectoryEntry[]>(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const [selected, setSelected] = useState<DirectoryEntry[]>(() =>
|
|
100
|
+
values.map(item => directoryEntryFromStored(kind, item))
|
|
101
|
+
);
|
|
99
102
|
|
|
100
103
|
useEffect(() => {
|
|
101
|
-
|
|
102
|
-
setSelected([]);
|
|
103
|
-
setResolveFailed(false);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
let active = true;
|
|
107
|
-
setResolving(true);
|
|
108
|
-
setResolveFailed(false);
|
|
109
|
-
void resolveDirectory(kind, values)
|
|
110
|
-
.then(page => {
|
|
111
|
-
if (active) setSelected(page.items);
|
|
112
|
-
})
|
|
113
|
-
.catch(() => {
|
|
114
|
-
if (active) setResolveFailed(true);
|
|
115
|
-
})
|
|
116
|
-
.finally(() => {
|
|
117
|
-
if (active) setResolving(false);
|
|
118
|
-
});
|
|
119
|
-
return () => {
|
|
120
|
-
active = false;
|
|
121
|
-
};
|
|
104
|
+
setSelected(values.map(item => directoryEntryFromStored(kind, item)));
|
|
122
105
|
}, [kind, values]);
|
|
123
106
|
|
|
124
107
|
const remove = (entryId: string) => {
|
|
125
108
|
const next = selected.filter(entry => entry.id !== entryId);
|
|
126
109
|
setSelected(next);
|
|
127
|
-
onChange?.(
|
|
128
|
-
multiple ? next.map(entry => entry.id) : next[0]?.id || undefined
|
|
129
|
-
);
|
|
110
|
+
onChange?.(directoryStoredValueFromEntries(next, multiple));
|
|
130
111
|
};
|
|
131
112
|
const confirm = (items: DirectoryEntry[]) => {
|
|
132
113
|
setSelected(items);
|
|
133
|
-
onChange?.(
|
|
134
|
-
multiple ? items.map(entry => entry.id) : items[0]?.id || undefined
|
|
135
|
-
);
|
|
114
|
+
onChange?.(directoryStoredValueFromEntries(items, multiple));
|
|
136
115
|
setOpen(false);
|
|
137
116
|
};
|
|
138
117
|
const panel = (
|
|
@@ -168,11 +147,7 @@ export function PlatformDirectoryPicker({
|
|
|
168
147
|
{kind === 'user' ? <UserOutlined /> : <ApartmentOutlined />}
|
|
169
148
|
</span>
|
|
170
149
|
<span className="oxa-directory-trigger-values">
|
|
171
|
-
{
|
|
172
|
-
<Typography.Text type="secondary">读取中…</Typography.Text>
|
|
173
|
-
) : resolveFailed ? (
|
|
174
|
-
<Typography.Text type="danger">目录解析失败</Typography.Text>
|
|
175
|
-
) : selected.length ? (
|
|
150
|
+
{selected.length ? (
|
|
176
151
|
selected.map(entry => (
|
|
177
152
|
<Tag
|
|
178
153
|
closable={!disabled}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Form } from 'antd';
|
|
2
|
+
import type {
|
|
3
|
+
DataFieldSurface,
|
|
4
|
+
ResourceReferenceValue,
|
|
5
|
+
} from 'openxiangda-contracts/browser';
|
|
6
|
+
import {
|
|
7
|
+
AuthoritativeSelector,
|
|
8
|
+
MobileAuthoritativeSelector,
|
|
9
|
+
} from '../../AuthoritativeSelector';
|
|
10
|
+
|
|
11
|
+
export function ResourceReferenceField({
|
|
12
|
+
field,
|
|
13
|
+
fieldCode,
|
|
14
|
+
resourceCode,
|
|
15
|
+
operation,
|
|
16
|
+
disabled,
|
|
17
|
+
mobile,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
}: {
|
|
21
|
+
field: DataFieldSurface;
|
|
22
|
+
fieldCode: string;
|
|
23
|
+
resourceCode?: string;
|
|
24
|
+
operation: 'create' | 'update';
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
mobile?: boolean;
|
|
27
|
+
value?: ResourceReferenceValue | ResourceReferenceValue[];
|
|
28
|
+
onChange?: (
|
|
29
|
+
value: ResourceReferenceValue | ResourceReferenceValue[] | undefined
|
|
30
|
+
) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const form = Form.useFormInstance();
|
|
33
|
+
const formValues = Form.useWatch([], form) as
|
|
34
|
+
| Record<string, unknown>
|
|
35
|
+
| undefined;
|
|
36
|
+
const bindings = Object.fromEntries(
|
|
37
|
+
(field.source?.filters || []).flatMap(filter =>
|
|
38
|
+
'binding' in filter
|
|
39
|
+
? [[filter.binding.field, formValues?.[filter.binding.field]]]
|
|
40
|
+
: []
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
const Selector = mobile
|
|
44
|
+
? MobileAuthoritativeSelector
|
|
45
|
+
: AuthoritativeSelector;
|
|
46
|
+
return (
|
|
47
|
+
<Selector
|
|
48
|
+
bindings={bindings}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
fieldCode={fieldCode}
|
|
51
|
+
multiple={field.type === 'resource-ref.multiple'}
|
|
52
|
+
onChange={next =>
|
|
53
|
+
onChange?.(
|
|
54
|
+
next as
|
|
55
|
+
| ResourceReferenceValue
|
|
56
|
+
| ResourceReferenceValue[]
|
|
57
|
+
| undefined
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
operation={operation}
|
|
61
|
+
placeholder={`搜索并选择${field.label}`}
|
|
62
|
+
resourceCode={resourceCode}
|
|
63
|
+
source="resource"
|
|
64
|
+
value={value}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|