@boarteam/boar-pack-common-frontend 2.0.1

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 (36) hide show
  1. package/package.json +50 -0
  2. package/src/components/Descriptions/Descriptions.tsx +166 -0
  3. package/src/components/Descriptions/DescriptionsCreateModal.tsx +65 -0
  4. package/src/components/Descriptions/descriptionTypes.ts +49 -0
  5. package/src/components/Descriptions/index.ts +5 -0
  6. package/src/components/Descriptions/useDescriptionColumns.ts +37 -0
  7. package/src/components/Inputs/DateRange.tsx +75 -0
  8. package/src/components/Inputs/MultiStringSelect.tsx +20 -0
  9. package/src/components/Inputs/NumberInputHandlingNewRecord.tsx +11 -0
  10. package/src/components/Inputs/NumberSwitcher.tsx +27 -0
  11. package/src/components/Inputs/Password.tsx +33 -0
  12. package/src/components/Inputs/RelationSelect.tsx +72 -0
  13. package/src/components/Inputs/SearchSelect.tsx +93 -0
  14. package/src/components/Inputs/filterDropdowns.tsx +103 -0
  15. package/src/components/Inputs/index.ts +9 -0
  16. package/src/components/Inputs/useCheckConnection.tsx +79 -0
  17. package/src/components/List/List.tsx +266 -0
  18. package/src/components/List/index.ts +3 -0
  19. package/src/components/List/listTypes.ts +31 -0
  20. package/src/components/QuestionMarkHint/QuestionMarkHint.tsx +33 -0
  21. package/src/components/QuestionMarkHint/index.ts +1 -0
  22. package/src/components/Table/BulkDeleteButton.tsx +55 -0
  23. package/src/components/Table/BulkEditButton.tsx +160 -0
  24. package/src/components/Table/Table.tsx +400 -0
  25. package/src/components/Table/index.ts +6 -0
  26. package/src/components/Table/tableTools.ts +168 -0
  27. package/src/components/Table/tableTypes.ts +127 -0
  28. package/src/components/Table/useColumnsSets.tsx +110 -0
  29. package/src/components/index.ts +5 -0
  30. package/src/index.ts +2 -0
  31. package/src/tools/WebsocketClient.ts +138 -0
  32. package/src/tools/index.ts +5 -0
  33. package/src/tools/numberTools.ts +6 -0
  34. package/src/tools/safetyRun.ts +5 -0
  35. package/src/tools/useFullscreen.tsx +62 -0
  36. package/src/tools/useTabs.ts +17 -0
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@boarteam/boar-pack-common-frontend",
3
+ "version": "2.0.1",
4
+ "description": "Common frontend package for Boar Pack",
5
+ "repository": "git@github.com:boarteam/boar-pack.git",
6
+ "author": "Andrew Balakirev <balakirev.andrey@gmail.com>",
7
+ "license": "MIT",
8
+ "main": "src/index",
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org/",
14
+ "access": "public"
15
+ },
16
+ "peerDependencies": {
17
+ "@ant-design/icons": "^4.8.3",
18
+ "@ant-design/pro-components": "^2.6.52",
19
+ "@ant-design/pro-table": "^3.15.1",
20
+ "@ant-design/pro-utils": "^2.15.5",
21
+ "antd": "^5.15.3",
22
+ "react": "^18.2.0",
23
+ "react-dom": "^18.2.0",
24
+ "react-intl": "^6.6.2",
25
+ "umi": "^4.1.5"
26
+ },
27
+ "dependencies": {
28
+ "@nestjsx/crud-request": "^5.0.0-alpha.3",
29
+ "lodash": "^4.17.21"
30
+ },
31
+ "devDependencies": {
32
+ "@ant-design/icons": "^4.8.3",
33
+ "@ant-design/pro-components": "^2.6.52",
34
+ "@ant-design/pro-table": "^3.15.1",
35
+ "@ant-design/pro-utils": "^2.15.5",
36
+ "@types/lodash": "^4.17.0",
37
+ "@types/react-dom": "^18.2.22",
38
+ "@umijs/plugin-locale": "^0.16.0",
39
+ "antd": "^5.15.3",
40
+ "react": "^18.2.0",
41
+ "react-dom": "^18.2.0",
42
+ "react-intl": "^6.6.2",
43
+ "typescript": "^5.4.5",
44
+ "umi": "^4.1.5"
45
+ },
46
+ "scripts": {
47
+ "yalc:push": "yalc push"
48
+ },
49
+ "gitHead": "392553082a2f9f0124d05eaa11d0cdb6079766e1"
50
+ }
@@ -0,0 +1,166 @@
1
+ import { ActionType } from "@ant-design/pro-table";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Button, Result, Tooltip } from "antd";
4
+ import { DeleteOutlined, StopOutlined } from "@ant-design/icons";
5
+ import { FormattedMessage, useIntl } from "react-intl";
6
+ import { TDescriptionsProps, TGetOneParams } from "./descriptionTypes";
7
+ import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
8
+ import { PageLoading, ProDescriptions } from "@ant-design/pro-components";
9
+ import { columnsToDescriptionItemProps } from "./useDescriptionColumns";
10
+ import pick from "lodash/pick";
11
+ import safetyRun from "../../tools/safetyRun";
12
+ import { buildJoinFields, collectFieldsFromColumns } from "../Table";
13
+ import { RowEditableConfig } from "@ant-design/pro-utils";
14
+ import { useForm } from "antd/es/form/Form";
15
+
16
+ const Descriptions = <Entity extends Record<string | symbol, any>,
17
+ CreateDto = Entity,
18
+ UpdateDto = Entity,
19
+ TPathParams = object,
20
+ >(
21
+ {
22
+ mainTitle,
23
+ entity,
24
+ getOne,
25
+ onUpdate,
26
+ pathParams,
27
+ idColumnName = 'id',
28
+ entityToUpdateDto,
29
+ afterSave,
30
+ actionRef: actionRefProp,
31
+ editable,
32
+ canEdit = false,
33
+ columns,
34
+ params,
35
+ onEntityChange,
36
+ ...rest
37
+ }: TDescriptionsProps<Entity,
38
+ CreateDto,
39
+ UpdateDto,
40
+ TPathParams> & Omit<ProDescriptionsProps<Entity>, 'columns'>
41
+ ) => {
42
+ const [form] = useForm<Entity>();
43
+ const actionRefComponent = useRef<ActionType>();
44
+ const actionRef = actionRefProp || actionRefComponent;
45
+ const intl = useIntl();
46
+ const [data, setData] = useState<Partial<Entity> | undefined>(entity);
47
+ const [loading, setLoading] = useState(false);
48
+
49
+ const sections = columnsToDescriptionItemProps(columns, mainTitle);
50
+
51
+ const queryParams = useMemo(() => {
52
+ const join = params?.join;
53
+ const queryParams: TGetOneParams & TPathParams = {
54
+ ...(pathParams ?? {} as TPathParams),
55
+ };
56
+
57
+ const { joinSelect, joinFields } = buildJoinFields(join);
58
+ queryParams.join = joinSelect;
59
+ queryParams.fields = collectFieldsFromColumns(
60
+ columns,
61
+ idColumnName,
62
+ joinFields,
63
+ );
64
+
65
+ return queryParams;
66
+ }, [params, pathParams]);
67
+
68
+ const requestData = async () => {
69
+ if (!getOne) {
70
+ return;
71
+ }
72
+
73
+ setLoading(true);
74
+
75
+ try {
76
+ const record = await getOne(queryParams);
77
+ onEntityChange?.(record);
78
+ setData(record ?? undefined);
79
+ } catch (e) {
80
+ console.error(e);
81
+ setData(undefined);
82
+ } finally {
83
+ setLoading(false);
84
+ }
85
+ }
86
+
87
+ const onSave: RowEditableConfig<Entity>['onSave'] = async (propName, record) => {
88
+ try {
89
+ await form.validateFields();
90
+ if (onUpdate && entityToUpdateDto) {
91
+ await onUpdate({
92
+ ...queryParams,
93
+ ...{} as Record<keyof Entity, string>, // todo: fix this
94
+ // @ts-ignore
95
+ requestBody: entityToUpdateDto(pick(record, [propName])),
96
+ });
97
+ }
98
+
99
+ setData(record);
100
+ if (typeof afterSave === 'function') {
101
+ await afterSave(record);
102
+ }
103
+ } catch (e) {
104
+ console.error(e);
105
+ }
106
+ }
107
+
108
+ useEffect(() => {
109
+ safetyRun(requestData());
110
+ }, [])
111
+
112
+ useEffect(() => {
113
+ setData(entity);
114
+ form.setFieldsValue(entity as Entity);
115
+ }, [entity])
116
+
117
+ if (loading) {
118
+ return <PageLoading />;
119
+ }
120
+
121
+ if (!data) {
122
+ return (
123
+ <Result
124
+ status="404"
125
+ title="404"
126
+ subTitle="The instrument is not found."
127
+ extra={<Button type="primary" href={'/liquidity/ecn-instruments'}>See list of instruments</Button>}
128
+ />
129
+ );
130
+ }
131
+
132
+ return (
133
+ <>
134
+ {sections.map((section, index) => (
135
+ <ProDescriptions<Entity>
136
+ // @ts-ignore-next-line
137
+ form={form}
138
+ key={index + String(pathParams?.[idColumnName as keyof TPathParams])}
139
+ title={section.title as React.ReactNode}
140
+ actionRef={actionRef}
141
+ size={"small"}
142
+ bordered
143
+ loading={loading}
144
+ style={{ marginBottom: 20 }}
145
+ labelStyle={{ width: '15%' }}
146
+ contentStyle={{ width: '20%' }}
147
+ dataSource={data as Entity}
148
+ editable={canEdit ? {
149
+ type: 'multiple',
150
+ onSave,
151
+ deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
152
+ onlyAddOneLineAlertMessage: intl.formatMessage({ id: 'table.onlyAddOneLineAlertMessage' }),
153
+ cancelText: <Tooltip title={intl.formatMessage({ id: 'table.cancelText' })}><StopOutlined /></Tooltip>,
154
+ deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
155
+ saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
156
+ ...editable,
157
+ }: undefined}
158
+ columns={section.columns}
159
+ {...rest}
160
+ />
161
+ ))}
162
+ </>
163
+ );
164
+ };
165
+
166
+ export default Descriptions;
@@ -0,0 +1,65 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { Button, Modal } from "antd";
3
+ import { TDescriptionsCreateModalProps } from "./descriptionTypes";
4
+ import { ProDescriptions } from "@ant-design/pro-components";
5
+ import { columnsToDescriptionItemProps } from "./useDescriptionColumns";
6
+ import { useForm } from "antd/es/form/Form";
7
+ import { buildFieldsFromColumns } from "../Table";
8
+
9
+ const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
10
+ idColumnName,
11
+ columns,
12
+ data,
13
+ onClose,
14
+ onSubmit,
15
+ ...rest
16
+ }: TDescriptionsCreateModalProps<Entity>) => {
17
+ const sections = columnsToDescriptionItemProps(columns, 'General');
18
+ const [form] = useForm();
19
+
20
+ const editableKeys = useMemo(() => {
21
+ return [
22
+ ...buildFieldsFromColumns(
23
+ columns,
24
+ idColumnName,
25
+ ),
26
+ ];
27
+ }, [columns, idColumnName]);
28
+
29
+ useEffect(() => {
30
+ data ? form.setFieldsValue(data) : form.resetFields();
31
+ }, [data]);
32
+
33
+ return (
34
+ <Modal
35
+ open={data !== undefined}
36
+ onCancel={onClose}
37
+ width='80%'
38
+ footer={[
39
+ <Button key='submit' type="primary" onClick={async () => form.validateFields().then(onSubmit)}>Create</Button>
40
+ ]}
41
+ >
42
+ {sections.map((section, index) => (
43
+ <ProDescriptions<Entity>
44
+ key={index + (Array.isArray(idColumnName) ? idColumnName.join('-') : idColumnName)}
45
+ title={section.title as React.ReactNode}
46
+ size={"small"}
47
+ bordered
48
+ column={2}
49
+ style={{ marginBottom: 20 }}
50
+ labelStyle={{ width: '15%' }}
51
+ contentStyle={{ width: '25%' }}
52
+ editable={{
53
+ form,
54
+ editableKeys,
55
+ actionRender: () => [],
56
+ }}
57
+ columns={section.columns}
58
+ {...rest}
59
+ />
60
+ ))}
61
+ </Modal>
62
+ );
63
+ };
64
+
65
+ export default DescriptionsCreateModal;
@@ -0,0 +1,49 @@
1
+ import { MutableRefObject } from "react";
2
+ import { ActionType } from "@ant-design/pro-table";
3
+ import { RowEditableConfig } from "@ant-design/pro-utils";
4
+ import { QueryJoin } from "@nestjsx/crud-request";
5
+ import { ProColumns, ProDescriptionsProps } from "@ant-design/pro-components";
6
+
7
+ export type TGetOneParams = {
8
+ /**
9
+ * Selects resource fields. <a href="https://github.com/nestjsx/crud/wiki/Requests#select" target="_blank">Docs</a>
10
+ */
11
+ fields?: Array<string>,
12
+ /**
13
+ * Adds relational resources. <a href="https://github.com/nestjsx/crud/wiki/Requests#join" target="_blank">Docs</a>
14
+ */
15
+ join?: Array<string>,
16
+ /**
17
+ * Reset cache (if was enabled). <a href="https://github.com/nestjsx/crud/wiki/Requests#cache" target="_blank">Docs</a>
18
+ */
19
+ cache?: number,
20
+ }
21
+ export type TDescriptionGetRequestParams = {
22
+ join?: QueryJoin | QueryJoin[];
23
+ };
24
+ export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
25
+ mainTitle?: ProColumns<Entity>['title'] | null,
26
+ entity?: Partial<Entity>,
27
+ getOne?: ({}: TGetOneParams & TPathParams) => Promise<Entity | null>,
28
+ onUpdate?: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>,
29
+ onDelete?: ({}: Record<keyof Entity, string> & TPathParams) => Promise<void>,
30
+ pathParams?: TPathParams,
31
+ idColumnName?: string & keyof Entity,
32
+ entityToUpdateDto?: (entity: Partial<Entity>) => UpdateDto,
33
+ createNewDefaultParams?: Partial<Entity>,
34
+ afterSave?: (record: Entity) => Promise<void>,
35
+ actionRef?: MutableRefObject<ActionType | undefined>,
36
+ editable?: RowEditableConfig<Entity>,
37
+ canEdit?: boolean,
38
+ params?: TDescriptionGetRequestParams,
39
+ columns: ProColumns<Entity>[],
40
+ onEntityChange?: (entity: Entity | null) => void;
41
+ }
42
+
43
+ export type TDescriptionsCreateModalProps<Entity> = Omit<ProDescriptionsProps<Entity>, 'columns'> & {
44
+ idColumnName: string & keyof Entity | (string & keyof Entity)[],
45
+ columns: ProColumns<Entity>[],
46
+ data: Partial<Entity> | undefined,
47
+ onSubmit: (data: Entity) => Promise<void>,
48
+ onClose: () => void,
49
+ }
@@ -0,0 +1,5 @@
1
+ export * from './Descriptions';
2
+ export { default as Descriptions } from './Descriptions';
3
+ export * from './DescriptionsCreateModal';
4
+ export * from './descriptionTypes';
5
+ export * from './useDescriptionColumns';
@@ -0,0 +1,37 @@
1
+ import { ProColumns } from "@ant-design/pro-components";
2
+ import { ProDescriptionsItemProps } from "@ant-design/pro-descriptions";
3
+
4
+ export type TDescriptionSection<T> = {
5
+ title: ProColumns<T>['title'] | null;
6
+ columns: ProDescriptionsItemProps<T>[];
7
+ }
8
+
9
+ export function columnsToDescriptionItemProps<T>(
10
+ columns: ProColumns<T>[],
11
+ mainTitle: ProColumns<T>['title'] | null = null,
12
+ ): TDescriptionSection<T>[] {
13
+ const baseSection: TDescriptionSection<T> = {
14
+ title: mainTitle,
15
+ columns: [],
16
+ }
17
+ const result: TDescriptionSection<T>[] = [baseSection];
18
+
19
+ columns.forEach((column) => {
20
+ if (column.valueType === 'option') {
21
+ return;
22
+ }
23
+
24
+ if (column.children) {
25
+ result.push(...columnsToDescriptionItemProps(column.children, column.title));
26
+ } else {
27
+ const {
28
+ children,
29
+ ...rest
30
+ } = column;
31
+ // @ts-ignore-next-line
32
+ baseSection.columns.push(rest as ProDescriptionsItemProps<T>);
33
+ }
34
+ });
35
+
36
+ return result;
37
+ }
@@ -0,0 +1,75 @@
1
+ // antd switch but not for boolean, for numbers 0 and 1
2
+ import React from "react";
3
+ import dayjs from "dayjs";
4
+ import { DatePicker } from "antd";
5
+
6
+ interface RangePickerProps extends Omit<React.ComponentProps<typeof DatePicker.RangePicker>, 'value' | 'onChange'> {
7
+ value? : [string | null, string | null] | null;
8
+ onChange? : (value?: [string | null, string | null] | null) => void;
9
+ }
10
+
11
+ export const DateRange: React.FC<RangePickerProps> = ({
12
+ value,
13
+ onChange,
14
+ ...props
15
+ }) => {
16
+ return (
17
+ <DatePicker.RangePicker
18
+ showTime={{
19
+ showNow: true,
20
+ showHour: true,
21
+ showMinute: true,
22
+ showSecond: true,
23
+ showMillisecond: true,
24
+ }}
25
+ allowEmpty={[true, true]}
26
+ presets={[
27
+ {
28
+ label: 'Today',
29
+ value: [dayjs().startOf('day'), dayjs().endOf('day')],
30
+ },
31
+ {
32
+ label: 'Yesterday',
33
+ value: [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
34
+ },
35
+ {
36
+ label: 'Last 15 minutes',
37
+ value: [dayjs().subtract(15, 'minute'), dayjs()],
38
+ },
39
+ {
40
+ label: 'Last 30 minutes',
41
+ value: [dayjs().subtract(30, 'minute'), dayjs()],
42
+ },
43
+ {
44
+ label: 'Last 1 hour',
45
+ value: [dayjs().subtract(1, 'hour'), dayjs()],
46
+ },
47
+ {
48
+ label: 'Last 24 hours',
49
+ value: [dayjs().subtract(1, 'day'), dayjs()],
50
+ },
51
+ {
52
+ label: 'Last 7 days',
53
+ value: [dayjs().subtract(7, 'day'), dayjs()],
54
+ },
55
+ {
56
+ label: 'Last 30 days',
57
+ value: [dayjs().subtract(30, 'day'), dayjs()],
58
+ },
59
+ {
60
+ label: 'This month',
61
+ value: [dayjs().startOf('month'), dayjs().endOf('month')],
62
+ },
63
+ {
64
+ label: 'Last month',
65
+ value: [dayjs().subtract(1, 'month').startOf('month'), dayjs().subtract(1, 'month').endOf('month')],
66
+ },
67
+ ]}
68
+ value={value?.map(date => dayjs(date)) as [dayjs.Dayjs, dayjs.Dayjs] | undefined}
69
+ onChange={(dates) => {
70
+ onChange?.(dates?.map(date => date?.toISOString() ?? null) as [string | null, string | null] | null);
71
+ }}
72
+ {...props}
73
+ />
74
+ );
75
+ }
@@ -0,0 +1,20 @@
1
+ import { Select } from "antd";
2
+ import React from "react";
3
+
4
+ export const MultiStringSelect: React.FC<{
5
+ value?: string[];
6
+ onChange?: (value: string[]) => void;
7
+ }> = ({ value, onChange }) => {
8
+ return (
9
+ <Select<string[]>
10
+ dropdownStyle={{ minWidth: 200 }}
11
+ allowClear={true}
12
+ mode="tags"
13
+ style={{ minWidth: 200 }}
14
+ maxTagCount='responsive'
15
+ value={value}
16
+ onChange={onChange}
17
+ notFoundContent='Enter manually'
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { InputNumberProps } from "antd/es/input-number";
3
+ import { InputNumber } from "antd";
4
+ import { isRecordNew } from "../Table";
5
+
6
+ export const NumberInputHandlingNewRecord: React.FC<InputNumberProps> = ({ value, onChange }) => {
7
+ return <InputNumber
8
+ value={isRecordNew({ id: value }) ? '' : value}
9
+ onChange={onChange}
10
+ />
11
+ }
@@ -0,0 +1,27 @@
1
+ // antd switch but not for boolean, for numbers 0 and 1
2
+ import { Switch } from "antd";
3
+ import React from "react";
4
+ import { SwitchProps } from "antd/es/switch";
5
+
6
+ interface NumberSwitcherProps extends Omit<SwitchProps, 'value' | 'onChange'> {
7
+ value?: number;
8
+ onChange?: (value?: number) => void;
9
+ }
10
+
11
+ export const NumberSwitch: React.FC<NumberSwitcherProps> = ({
12
+ value,
13
+ onChange,
14
+ ...props
15
+ }) => {
16
+ return (
17
+ <Switch
18
+ checkedChildren="1"
19
+ unCheckedChildren="0"
20
+ checked={value === 1}
21
+ onChange={(checked) => {
22
+ onChange?.(checked ? 1 : 0);
23
+ }}
24
+ {...props}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { Button, Input, Tooltip } from "antd";
3
+ import { ThunderboltOutlined } from "@ant-design/icons";
4
+ import { Typography } from "antd";
5
+ import { PasswordProps } from "antd/es/input/Password";
6
+
7
+ const { Paragraph } = Typography;
8
+
9
+ export const Password: React.FC<PasswordProps> = ({ value, onChange }) => {
10
+ return (
11
+ <Input.Group compact={true}>
12
+ <Input.Password
13
+ placeholder={'Enter password'}
14
+ onChange={onChange}
15
+ value={value}
16
+ style={{ width: 'calc(100% - 80px)' }}
17
+ autoComplete={'one-time-code'}
18
+ />
19
+ <Tooltip title="Generate password">
20
+ <Button
21
+ onClick={(e) => {
22
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%&';
23
+ const pass = Array(10).fill('').map(x => chars[Math.floor(Math.random() * chars.length)]).join('');
24
+ // @ts-ignore
25
+ onChange?.(pass);
26
+ }}
27
+ icon={<ThunderboltOutlined color={'var(--ant-primary)'} />}
28
+ />
29
+ </Tooltip>
30
+ <Button icon={<Paragraph copyable={{ text: String(value) }} />} />
31
+ </Input.Group>
32
+ );
33
+ }
@@ -0,0 +1,72 @@
1
+ import { ProFormSelectProps } from "@ant-design/pro-form/lib/components/Select";
2
+ import { useState } from "react";
3
+ import { ProFormSelect } from "@ant-design/pro-form";
4
+
5
+ type RelationSelectProps<T> = ProFormSelectProps & {
6
+ selectedItem: T | null | undefined,
7
+ onChange?: (type: T | null) => void,
8
+ filter?: string[],
9
+ fetchItems: (filter: string[], keyword?: string) => Promise<{data: T[]}>,
10
+ fieldNames?: {
11
+ value: string,
12
+ label: string,
13
+ },
14
+ };
15
+
16
+ export const RelationSelect = function<T>({
17
+ selectedItem,
18
+ onChange,
19
+ filter = [],
20
+ fetchItems,
21
+ fieldNames = {
22
+ value: 'id',
23
+ label: 'name',
24
+ },
25
+ ...rest
26
+ }: RelationSelectProps<T>) {
27
+ const { value: valueKey, label: labelKey } = fieldNames;
28
+ const [value, setValue] = useState(selectedItem ? {
29
+ label: selectedItem[labelKey as keyof T],
30
+ value: selectedItem[valueKey as keyof T],
31
+ } : undefined);
32
+
33
+ const request = async ({ keyWords: keyword }: { keyWords: string }) => {
34
+ const reqFilter = [...filter];
35
+ if (keyword) {
36
+ reqFilter.push(labelKey + '||$contL||' + keyword);
37
+ }
38
+ const resp = await fetchItems(reqFilter, keyword);
39
+ return resp.data;
40
+ }
41
+
42
+ return (
43
+ <ProFormSelect.SearchSelect
44
+ showSearch
45
+ mode={'single'}
46
+ request={request}
47
+ formItemProps={{
48
+ // correct color for invalid relational fields (#64)
49
+ // @ts-ignore-next-line
50
+ validateStatus: rest['aria-invalid'] === 'true' ? 'error' : 'success',
51
+ style: {
52
+ margin: 0,
53
+ display: 'inline-block',
54
+ }
55
+ }}
56
+ style={{ minWidth: 160 }}
57
+ placeholder='Please choose'
58
+ fieldProps={{
59
+ fieldNames: {
60
+ value: valueKey,
61
+ label: labelKey,
62
+ },
63
+ value,
64
+ onChange(value, row) {
65
+ setValue(value);
66
+ onChange?.(row ? value : null);
67
+ },
68
+ }}
69
+ {...rest}
70
+ />
71
+ );
72
+ }