@boarteam/boar-pack-common-frontend 2.6.0-alpha.2 → 2.6.0-alpha.3
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 +2 -2
- package/src/components/Descriptions/Descriptions.tsx +20 -20
- package/src/components/Descriptions/descriptionTypes.ts +4 -2
- package/src/components/Inputs/RelationSelect.tsx +2 -5
- package/src/components/Table/CreateEntityModal.tsx +4 -4
- package/src/components/Table/Table.tsx +15 -1
- package/src/components/Table/tableTools.ts +17 -0
- package/src/components/Table/tableTypes.ts +2 -0
- package/src/components/Table/useColumnsSets.tsx +40 -7
- package/src/components/Table/useCreation.tsx +19 -1
- package/src/components/Table/useEditableTable.tsx +2 -0
- package/src/components/Table/useImportExport.tsx +58 -0
- package/src/tools/ApiError.ts +18 -0
- package/src/tools/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boarteam/boar-pack-common-frontend",
|
|
3
|
-
"version": "2.6.0-alpha.
|
|
3
|
+
"version": "2.6.0-alpha.3",
|
|
4
4
|
"description": "Common frontend package for Boar Pack",
|
|
5
5
|
"repository": "git@github.com:boarteam/boar-pack.git",
|
|
6
6
|
"author": "Andrew Balakirev <balakirev.andrey@gmail.com>",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"yalc:push": "yalc push"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "8e43d50ded7a9d9554b6624c89dc8a038f82c8d2"
|
|
50
50
|
}
|
|
@@ -15,26 +15,24 @@ import useContentViewMode, { VIEW_MODE_TYPE } from "./useContentViewMode";
|
|
|
15
15
|
import { createStyles } from "antd-style";
|
|
16
16
|
import { debounce } from "lodash";
|
|
17
17
|
import { NamePath } from "antd/lib/form/interface";
|
|
18
|
+
import { FieldData } from "rc-field-form/lib/interface";
|
|
18
19
|
|
|
19
|
-
const useStyles = createStyles(() => {
|
|
20
|
+
const useStyles = createStyles(({css}) => {
|
|
20
21
|
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'.anticon-edit': {
|
|
34
|
-
opacity: 1
|
|
35
|
-
},
|
|
22
|
+
antDescriptionsStyles: css`
|
|
23
|
+
.ant-descriptions-item-content {
|
|
24
|
+
.anticon-edit {
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transition: opacity 200ms;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
.anticon-edit {
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
36
34
|
}
|
|
37
|
-
|
|
35
|
+
`
|
|
38
36
|
}
|
|
39
37
|
})
|
|
40
38
|
|
|
@@ -64,7 +62,7 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
|
|
|
64
62
|
CreateDto,
|
|
65
63
|
UpdateDto,
|
|
66
64
|
TPathParams>,
|
|
67
|
-
ref: React.Ref<DescriptionsRefType
|
|
65
|
+
ref: React.Ref<DescriptionsRefType<Entity>>,
|
|
68
66
|
) => {
|
|
69
67
|
const { styles } = useStyles();
|
|
70
68
|
|
|
@@ -136,6 +134,9 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
|
|
|
136
134
|
form.resetFields();
|
|
137
135
|
},
|
|
138
136
|
submit: () => handleSubmit(),
|
|
137
|
+
setFieldErrors: (fields: FieldData<Entity>[]) => {
|
|
138
|
+
form.setFields(fields)
|
|
139
|
+
}
|
|
139
140
|
}));
|
|
140
141
|
|
|
141
142
|
const onValuesChange = debounce((changedValues, allValues) => {
|
|
@@ -240,8 +241,7 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
|
|
|
240
241
|
<Result
|
|
241
242
|
status="404"
|
|
242
243
|
title="404"
|
|
243
|
-
subTitle="The
|
|
244
|
-
extra={<Button type="primary" href={'/liquidity/ecn-instruments'}>See list of instruments</Button>}
|
|
244
|
+
subTitle="The entity is not found."
|
|
245
245
|
/>
|
|
246
246
|
);
|
|
247
247
|
}
|
|
@@ -4,6 +4,7 @@ import { RowEditableConfig } from "@ant-design/pro-utils";
|
|
|
4
4
|
import { QueryJoin } from "@nestjsx/crud-request";
|
|
5
5
|
import { ProColumns } from "@ant-design/pro-components";
|
|
6
6
|
import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
|
|
7
|
+
import { FieldData } from "rc-field-form/lib/interface";
|
|
7
8
|
|
|
8
9
|
export type TGetOneParams = {
|
|
9
10
|
/**
|
|
@@ -23,9 +24,10 @@ export type TDescriptionGetRequestParams = {
|
|
|
23
24
|
join?: QueryJoin | QueryJoin[];
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
export type DescriptionsRefType = {
|
|
27
|
+
export type DescriptionsRefType<Entity> = {
|
|
27
28
|
reset: () => void;
|
|
28
29
|
submit: () => void;
|
|
30
|
+
setFieldErrors: (fields: FieldData<Entity>[]) => void;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
|
|
@@ -49,7 +51,7 @@ export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = objec
|
|
|
49
51
|
params?: TDescriptionGetRequestParams,
|
|
50
52
|
columns: ProColumns<Entity>[],
|
|
51
53
|
onEntityChange?: (entity: Entity | null) => void;
|
|
52
|
-
ref?: React.Ref<DescriptionsRefType
|
|
54
|
+
ref?: React.Ref<DescriptionsRefType<Entity>>,
|
|
53
55
|
} & Omit<ProDescriptionsProps<Entity>, 'columns'>;
|
|
54
56
|
|
|
55
57
|
export type TDescriptionsCreateModalProps<Entity> = Omit<ProDescriptionsProps<Entity>, 'columns'> & {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { ProFormSelectProps } from "@ant-design/pro-
|
|
1
|
+
import { ProFormSelect, ProFormSelectProps } from "@ant-design/pro-components";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { ProFormSelect } from "@ant-design/pro-form";
|
|
4
3
|
|
|
5
4
|
type RelationSelectProps<T> = ProFormSelectProps & {
|
|
6
5
|
selectedItem: T | null | undefined,
|
|
@@ -44,10 +43,8 @@ export const RelationSelect = function<T>({
|
|
|
44
43
|
showSearch
|
|
45
44
|
mode={'single'}
|
|
46
45
|
request={request}
|
|
46
|
+
className='relational-select'
|
|
47
47
|
formItemProps={{
|
|
48
|
-
// correct color for invalid relational fields (#64)
|
|
49
|
-
// @ts-ignore-next-line
|
|
50
|
-
validateStatus: rest['aria-invalid'] === 'true' ? 'error' : 'success',
|
|
51
48
|
style: {
|
|
52
49
|
margin: 0,
|
|
53
50
|
display: 'inline-block',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProColumns } from "@ant-design/pro-components";
|
|
2
2
|
import { Button, Modal } from "antd";
|
|
3
|
-
import { useRef } from "react";
|
|
3
|
+
import { MutableRefObject, useRef } from "react";
|
|
4
4
|
import { Descriptions, DescriptionsRefType } from "../Descriptions";
|
|
5
5
|
import { buildFieldsFromColumnsForDescriptionsDisplay } from "./tableTools";
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export interface CreateEntityModalProps<Entity> {
|
|
|
23
23
|
* Called when the form is submitted.
|
|
24
24
|
* Receives the validated form data.
|
|
25
25
|
*/
|
|
26
|
-
onSubmit: (data: any) => Promise<void>;
|
|
26
|
+
onSubmit: (data: any, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => Promise<void>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function CreateEntityModal<
|
|
@@ -41,7 +41,7 @@ export function CreateEntityModal<
|
|
|
41
41
|
onCancel,
|
|
42
42
|
onSubmit,
|
|
43
43
|
}: CreateEntityModalProps<Entity>) {
|
|
44
|
-
const descriptionsRef = useRef<DescriptionsRefType
|
|
44
|
+
const descriptionsRef = useRef<DescriptionsRefType<Entity>>(null);
|
|
45
45
|
|
|
46
46
|
// Calculate the editable keys from the columns and idColumnName
|
|
47
47
|
const editableKeys = [...buildFieldsFromColumnsForDescriptionsDisplay(columns, idColumnName)];
|
|
@@ -74,7 +74,7 @@ export function CreateEntityModal<
|
|
|
74
74
|
labelStyle={{ width: '15%' }}
|
|
75
75
|
contentStyle={{ width: '25%' }}
|
|
76
76
|
canEdit={true}
|
|
77
|
-
onCreate={onSubmit}
|
|
77
|
+
onCreate={(data) => onSubmit(data, descriptionsRef)}
|
|
78
78
|
editable={{
|
|
79
79
|
editableKeys,
|
|
80
80
|
actionRender: () => [],
|
|
@@ -9,6 +9,7 @@ import { KEY_SYMBOL, useCreation } from "./useCreation";
|
|
|
9
9
|
import { getTableDataQueryParams } from "./getTableDataQueryParams";
|
|
10
10
|
import { useEditableTable } from "./useEditableTable";
|
|
11
11
|
import { useBulkEditing } from "./useBulkEditing";
|
|
12
|
+
import { useImportExport } from "./useImportExport";
|
|
12
13
|
|
|
13
14
|
const useStyles = createStyles(() => {
|
|
14
15
|
return {
|
|
@@ -34,6 +35,8 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
34
35
|
onUpdateMany,
|
|
35
36
|
onDelete,
|
|
36
37
|
onDeleteMany,
|
|
38
|
+
exportUrl,
|
|
39
|
+
onImport,
|
|
37
40
|
pathParams,
|
|
38
41
|
idColumnName = 'id',
|
|
39
42
|
entityToCreateDto,
|
|
@@ -118,6 +121,11 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
118
121
|
createNewDefaultParams,
|
|
119
122
|
});
|
|
120
123
|
|
|
124
|
+
const { exportButton, importButton, setLastQueryParams } = useImportExport<TPathParams>({
|
|
125
|
+
exportUrl,
|
|
126
|
+
onImport,
|
|
127
|
+
})
|
|
128
|
+
|
|
121
129
|
useEffect(() => {
|
|
122
130
|
setUpdatePopupData(editableRecord);
|
|
123
131
|
actionRef?.current?.reload();
|
|
@@ -158,6 +166,7 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
158
166
|
queryParams,
|
|
159
167
|
result,
|
|
160
168
|
]);
|
|
169
|
+
setLastQueryParams(queryParams);
|
|
161
170
|
return result;
|
|
162
171
|
}
|
|
163
172
|
|
|
@@ -181,10 +190,13 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
181
190
|
listsHeight: 500,
|
|
182
191
|
},
|
|
183
192
|
}}
|
|
193
|
+
scroll={{
|
|
194
|
+
x: 'max-content',
|
|
195
|
+
}}
|
|
184
196
|
bordered
|
|
185
197
|
search={false}
|
|
186
198
|
editable={editableConfig}
|
|
187
|
-
toolBarRender={(...args) => [
|
|
199
|
+
toolBarRender={toolBarRender === false ? false : (...args) => [
|
|
188
200
|
columnsSetSelect?.() || null,
|
|
189
201
|
!viewOnly && onUpdateMany
|
|
190
202
|
? bulkEditButton
|
|
@@ -193,6 +205,8 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
193
205
|
? bulkDeleteButton
|
|
194
206
|
: null,
|
|
195
207
|
!viewOnly && createButton || null,
|
|
208
|
+
!viewOnly && onImport && importButton || null,
|
|
209
|
+
exportUrl && exportButton || null,
|
|
196
210
|
...toolBarRender && toolBarRender(...args) || [],
|
|
197
211
|
]}
|
|
198
212
|
columns={columns}
|
|
@@ -58,6 +58,22 @@ export function getFiltersSearch({
|
|
|
58
58
|
value = true;
|
|
59
59
|
}
|
|
60
60
|
break;
|
|
61
|
+
|
|
62
|
+
case Operators.in:
|
|
63
|
+
case Operators.inLow:
|
|
64
|
+
if (Array.isArray(value) && value.length === 1 && value[0] === null) {
|
|
65
|
+
operator = Operators.isNull;
|
|
66
|
+
value = true;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case Operators.equals:
|
|
71
|
+
if (Array.isArray(value) && value.length === 1 && value[0] === null || value === null) {
|
|
72
|
+
operator = Operators.isNull;
|
|
73
|
+
value = true;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
search.$and?.push({ [field]: { [operator]: value } });
|
|
@@ -81,6 +97,7 @@ export const Operators = {
|
|
|
81
97
|
lowerOrEquals: CondOperator.LOWER_THAN_EQUALS,
|
|
82
98
|
isNull: CondOperator.IS_NULL,
|
|
83
99
|
notNull: CondOperator.NOT_NULL,
|
|
100
|
+
starts: CondOperator.STARTS,
|
|
84
101
|
} as const;
|
|
85
102
|
|
|
86
103
|
export function applyKeywordToSearch(
|
|
@@ -107,6 +107,8 @@ export interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
|
|
|
107
107
|
editable?: RowEditableConfig<Entity>;
|
|
108
108
|
afterSave?: (record: Entity) => Promise<void>;
|
|
109
109
|
onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
|
|
110
|
+
exportUrl?: string;
|
|
111
|
+
onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
|
|
110
112
|
onUpdate: ({}: Partial<Entity> & {
|
|
111
113
|
requestBody: UpdateDto,
|
|
112
114
|
index?: number,
|
|
@@ -33,19 +33,29 @@ function getColumnsStates<T>(
|
|
|
33
33
|
columns: TIndexableRecord[],
|
|
34
34
|
shownCols: Set<keyof T>,
|
|
35
35
|
state: TColumnsState = {},
|
|
36
|
-
): Record<string, ColumnsState
|
|
36
|
+
): {state: Record<string, ColumnsState>, someColumnsShown: boolean} {
|
|
37
|
+
let someColumnsShown = false;
|
|
37
38
|
columns.forEach(col => {
|
|
38
39
|
const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex;
|
|
40
|
+
let childrenColumnsShown = false;
|
|
39
41
|
if ('children' in col && Array.isArray(col.children)) {
|
|
40
|
-
getColumnsStates(col.children, shownCols, state);
|
|
42
|
+
const { someColumnsShown } = getColumnsStates(col.children, shownCols, state);
|
|
43
|
+
if (someColumnsShown) {
|
|
44
|
+
childrenColumnsShown = true;
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
if (idx
|
|
44
|
-
|
|
48
|
+
if (idx) {
|
|
49
|
+
if (shownCols.has(idx as keyof T) || childrenColumnsShown) {
|
|
50
|
+
state[idx as string] = { show: true };
|
|
51
|
+
someColumnsShown = true;
|
|
52
|
+
} else {
|
|
53
|
+
state[idx as string] = { show: false };
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
}, state);
|
|
47
57
|
|
|
48
|
-
return state
|
|
58
|
+
return { state, someColumnsShown };
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export default function useColumnsSets<Entity>({
|
|
@@ -60,7 +70,7 @@ export default function useColumnsSets<Entity>({
|
|
|
60
70
|
columns: columnsSet
|
|
61
71
|
}) => [
|
|
62
72
|
name,
|
|
63
|
-
getColumnsStates<Entity>(columns, new Set(columnsSet))
|
|
73
|
+
getColumnsStates<Entity>(columns as TIndexableRecord[], new Set(columnsSet)).state,
|
|
64
74
|
])
|
|
65
75
|
), [columns]
|
|
66
76
|
);
|
|
@@ -97,7 +107,30 @@ export default function useColumnsSets<Entity>({
|
|
|
97
107
|
|
|
98
108
|
const columnsState = {
|
|
99
109
|
value: chosenColumnsSet,
|
|
100
|
-
|
|
110
|
+
// value contains only hidden columns and one which is just changed
|
|
111
|
+
onChange: (value: TColumnsStates) => {
|
|
112
|
+
const checkParentVisibility = (columns: TIndexableRecord[]) => {
|
|
113
|
+
let someColumnsShown = false;
|
|
114
|
+
|
|
115
|
+
columns.forEach(col => {
|
|
116
|
+
const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex as string;
|
|
117
|
+
if (idx && value[idx]?.show) {
|
|
118
|
+
someColumnsShown = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ('children' in col && Array.isArray(col.children)) {
|
|
122
|
+
const someChildColumnsShown = checkParentVisibility(col.children);
|
|
123
|
+
if (someChildColumnsShown) {
|
|
124
|
+
value[idx] = { show: true };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return someColumnsShown;
|
|
130
|
+
};
|
|
131
|
+
checkParentVisibility(columns as TIndexableRecord[]);
|
|
132
|
+
setChosenColumnsSet(value);
|
|
133
|
+
},
|
|
101
134
|
};
|
|
102
135
|
|
|
103
136
|
return {
|
|
@@ -5,6 +5,8 @@ import { PlusOutlined } from "@ant-design/icons";
|
|
|
5
5
|
import { FormattedMessage } from "react-intl";
|
|
6
6
|
import type { SizeType } from "antd/es/config-provider/SizeContext";
|
|
7
7
|
import { CreateEntityModal, CreateEntityModalProps } from "./CreateEntityModal";
|
|
8
|
+
import { DescriptionsRefType } from "../Descriptions";
|
|
9
|
+
import { ApiError } from '../../tools'
|
|
8
10
|
|
|
9
11
|
let creatingRecordsCount = 0;
|
|
10
12
|
export const KEY_SYMBOL = Symbol('key');
|
|
@@ -41,7 +43,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
|
|
|
41
43
|
} & Omit<CreateEntityModalProps<Entity>, 'onSubmit' | 'onCancel' | 'entity'>) {
|
|
42
44
|
const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
|
|
43
45
|
|
|
44
|
-
const onCreateSubmit = async (data: Entity) => {
|
|
46
|
+
const onCreateSubmit = async (data: Entity, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
|
|
45
47
|
try {
|
|
46
48
|
await onCreate?.({
|
|
47
49
|
...pathParams,
|
|
@@ -54,6 +56,22 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
|
|
|
54
56
|
await actionRef?.current?.reload();
|
|
55
57
|
} catch (e) {
|
|
56
58
|
console.error(e);
|
|
59
|
+
|
|
60
|
+
// Handle common error
|
|
61
|
+
if (e.body && e.body.statusCode && e.body.errors) {
|
|
62
|
+
const error = e as ApiError;
|
|
63
|
+
const { statusCode, errors } = error.body;
|
|
64
|
+
// Validation error. Highlight corresponding form fields
|
|
65
|
+
if (statusCode === 400) {
|
|
66
|
+
const formErrors = errors.map(error => ({
|
|
67
|
+
name: error.field,
|
|
68
|
+
errors: [error.message],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
descriptionsRef.current.setFieldErrors(formErrors);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
}
|
|
58
76
|
};
|
|
59
77
|
|
|
@@ -5,6 +5,7 @@ import { Button, Tooltip } from "antd";
|
|
|
5
5
|
import { DeleteOutlined, StopOutlined } from "@ant-design/icons";
|
|
6
6
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
7
7
|
import React, { useState } from "react";
|
|
8
|
+
import { isRecordNew } from "./useCreation";
|
|
8
9
|
|
|
9
10
|
export function useEditableTable<Entity, CreateDto, UpdateDto, TPathParams = {}>(
|
|
10
11
|
{
|
|
@@ -68,6 +69,7 @@ export function useEditableTable<Entity, CreateDto, UpdateDto, TPathParams = {}>
|
|
|
68
69
|
}
|
|
69
70
|
},
|
|
70
71
|
async onDelete(id, row) {
|
|
72
|
+
if (isRecordNew(row)) return;
|
|
71
73
|
await onDelete({ ...row, ...pathParams });
|
|
72
74
|
},
|
|
73
75
|
deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button, Tooltip } from 'antd';
|
|
2
|
+
import { DownloadOutlined, UploadOutlined } from "@ant-design/icons";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { TGetAllParams } from "./tableTypes";
|
|
5
|
+
import { Link } from "react-router-dom";
|
|
6
|
+
|
|
7
|
+
export function useImportExport<TPathParams = {}>({
|
|
8
|
+
exportUrl,
|
|
9
|
+
onImport
|
|
10
|
+
}: {
|
|
11
|
+
exportUrl?: string;
|
|
12
|
+
onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
|
|
13
|
+
}) {
|
|
14
|
+
const [isLoadingImport, setIsLoadingImport] = useState(false);
|
|
15
|
+
const [lastQueryParams, setLastQueryParams] = useState<TGetAllParams & TPathParams>();
|
|
16
|
+
|
|
17
|
+
const onImportChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
18
|
+
setIsLoadingImport(true);
|
|
19
|
+
await onImport?.(event)
|
|
20
|
+
.then((response) => {
|
|
21
|
+
console.log(response);
|
|
22
|
+
})
|
|
23
|
+
.finally(() => {
|
|
24
|
+
setIsLoadingImport(false);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = exportUrl + (lastQueryParams ? '?' + new URLSearchParams({
|
|
29
|
+
s: lastQueryParams.s,
|
|
30
|
+
sort: lastQueryParams.sort?.[0],
|
|
31
|
+
}).toString() : '');
|
|
32
|
+
const exportButton = <Tooltip title="Export">
|
|
33
|
+
<Link to={url} target={'_blank'}>
|
|
34
|
+
<Button icon={<DownloadOutlined />}/>
|
|
35
|
+
</Link>
|
|
36
|
+
</Tooltip>;
|
|
37
|
+
|
|
38
|
+
const importButton = <>
|
|
39
|
+
<Tooltip title="Import">
|
|
40
|
+
<label htmlFor="import-input">
|
|
41
|
+
<Button loading={isLoadingImport} icon={<UploadOutlined />} />
|
|
42
|
+
</label>
|
|
43
|
+
</Tooltip>
|
|
44
|
+
<input
|
|
45
|
+
type="file"
|
|
46
|
+
id="import-input"
|
|
47
|
+
style={{ display: "none" }}
|
|
48
|
+
accept=".xlsx, .xls"
|
|
49
|
+
onChange={onImportChange}
|
|
50
|
+
/>
|
|
51
|
+
</>
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
exportButton,
|
|
55
|
+
importButton,
|
|
56
|
+
setLastQueryParams
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Should be synced with common-backend/src/tools/ApiError.ts
|
|
2
|
+
type TApiErrorBodyType = {
|
|
3
|
+
statusCode: number
|
|
4
|
+
message: string
|
|
5
|
+
errors: {
|
|
6
|
+
field: string,
|
|
7
|
+
message: string
|
|
8
|
+
}[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Copied from api-client/generated/core/ApiError.ts
|
|
12
|
+
export class ApiError extends Error {
|
|
13
|
+
public readonly url: string;
|
|
14
|
+
public readonly status: number;
|
|
15
|
+
public readonly statusText: string;
|
|
16
|
+
public readonly body: TApiErrorBodyType;
|
|
17
|
+
public readonly request: any;
|
|
18
|
+
}
|
package/src/tools/index.ts
CHANGED