@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.
- package/package.json +50 -0
- package/src/components/Descriptions/Descriptions.tsx +166 -0
- package/src/components/Descriptions/DescriptionsCreateModal.tsx +65 -0
- package/src/components/Descriptions/descriptionTypes.ts +49 -0
- package/src/components/Descriptions/index.ts +5 -0
- package/src/components/Descriptions/useDescriptionColumns.ts +37 -0
- package/src/components/Inputs/DateRange.tsx +75 -0
- package/src/components/Inputs/MultiStringSelect.tsx +20 -0
- package/src/components/Inputs/NumberInputHandlingNewRecord.tsx +11 -0
- package/src/components/Inputs/NumberSwitcher.tsx +27 -0
- package/src/components/Inputs/Password.tsx +33 -0
- package/src/components/Inputs/RelationSelect.tsx +72 -0
- package/src/components/Inputs/SearchSelect.tsx +93 -0
- package/src/components/Inputs/filterDropdowns.tsx +103 -0
- package/src/components/Inputs/index.ts +9 -0
- package/src/components/Inputs/useCheckConnection.tsx +79 -0
- package/src/components/List/List.tsx +266 -0
- package/src/components/List/index.ts +3 -0
- package/src/components/List/listTypes.ts +31 -0
- package/src/components/QuestionMarkHint/QuestionMarkHint.tsx +33 -0
- package/src/components/QuestionMarkHint/index.ts +1 -0
- package/src/components/Table/BulkDeleteButton.tsx +55 -0
- package/src/components/Table/BulkEditButton.tsx +160 -0
- package/src/components/Table/Table.tsx +400 -0
- package/src/components/Table/index.ts +6 -0
- package/src/components/Table/tableTools.ts +168 -0
- package/src/components/Table/tableTypes.ts +127 -0
- package/src/components/Table/useColumnsSets.tsx +110 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/tools/WebsocketClient.ts +138 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/numberTools.ts +6 -0
- package/src/tools/safetyRun.ts +5 -0
- package/src/tools/useFullscreen.tsx +62 -0
- 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,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
|
+
}
|