@boarteam/boar-pack-common-frontend 2.4.0 → 2.5.0
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 +198 -46
- package/src/components/Descriptions/DescriptionsCreateModal.tsx +1 -1
- package/src/components/Descriptions/descriptionTypes.ts +20 -6
- package/src/components/Descriptions/useContentViewMode.tsx +28 -0
- package/src/components/Descriptions/useDescriptionColumns.ts +6 -1
- package/src/components/Table/BulkDeleteButton.tsx +2 -2
- package/src/components/Table/BulkEditButton.tsx +2 -2
- package/src/components/Table/CreateEntityModal.tsx +85 -0
- package/src/components/Table/DeleteButton.tsx +44 -0
- package/src/components/Table/Table.tsx +103 -266
- package/src/components/Table/getTableDataQueryParams.ts +79 -0
- package/src/components/Table/index.ts +1 -0
- package/src/components/Table/tableTools.ts +3 -2
- package/src/components/Table/tableTypes.ts +16 -9
- package/src/components/Table/useBulkEditing.tsx +128 -0
- package/src/components/Table/useCreation.tsx +96 -0
- package/src/components/Table/useEditableTable.tsx +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boarteam/boar-pack-common-frontend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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": "8eb798bf7672983cd13372098b1cfae43d73d190"
|
|
50
50
|
}
|
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
import { ActionType } from "@ant-design/pro-table";
|
|
2
|
-
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Button, Result, Tooltip } from "antd";
|
|
2
|
+
import React, { Key, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Badge, Button, Result, Tabs, TabsProps, Tooltip } from "antd";
|
|
4
4
|
import { DeleteOutlined, StopOutlined } from "@ant-design/icons";
|
|
5
5
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
6
|
-
import { TDescriptionsProps, TGetOneParams } from "./descriptionTypes";
|
|
7
|
-
import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
|
|
6
|
+
import { DescriptionsRefType, TDescriptionsProps, TGetOneParams } from "./descriptionTypes";
|
|
8
7
|
import { PageLoading, ProDescriptions } from "@ant-design/pro-components";
|
|
9
|
-
import { columnsToDescriptionItemProps } from "./useDescriptionColumns";
|
|
8
|
+
import { columnsToDescriptionItemProps, TDescriptionSection } from "./useDescriptionColumns";
|
|
10
9
|
import pick from "lodash/pick";
|
|
11
10
|
import safetyRun from "../../tools/safetyRun";
|
|
12
11
|
import { buildJoinFields, collectFieldsFromColumns } from "../Table";
|
|
13
12
|
import { RowEditableConfig } from "@ant-design/pro-utils";
|
|
14
13
|
import { useForm } from "antd/es/form/Form";
|
|
14
|
+
import useContentViewMode, { VIEW_MODE_TYPE } from "./useContentViewMode";
|
|
15
|
+
import { createStyles } from "antd-style";
|
|
16
|
+
import { debounce } from "lodash";
|
|
17
|
+
import { NamePath } from "antd/lib/form/interface";
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
const useStyles = createStyles(() => {
|
|
20
|
+
return {
|
|
21
|
+
/**
|
|
22
|
+
* Styles for the ant-descriptions component to show edit icon on hover
|
|
23
|
+
*/
|
|
24
|
+
antDescriptionsStyles: {
|
|
25
|
+
'.anticon-edit': {
|
|
26
|
+
opacity: 0,
|
|
27
|
+
transition: 'opacity 200ms'
|
|
28
|
+
},
|
|
29
|
+
'.ant-descriptions-item-content': {
|
|
30
|
+
width: '20%',
|
|
31
|
+
},
|
|
32
|
+
'.ant-descriptions-item-content:hover': {
|
|
33
|
+
'.anticon-edit': {
|
|
34
|
+
opacity: 1
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
|
|
17
42
|
CreateDto = Entity,
|
|
18
43
|
UpdateDto = Entity,
|
|
19
44
|
TPathParams = object,
|
|
20
|
-
|
|
45
|
+
>(
|
|
21
46
|
{
|
|
22
|
-
mainTitle,
|
|
47
|
+
mainTitle = 'General',
|
|
23
48
|
entity,
|
|
24
49
|
getOne,
|
|
25
50
|
onUpdate,
|
|
51
|
+
onCreate,
|
|
26
52
|
pathParams,
|
|
27
53
|
idColumnName = 'id',
|
|
28
54
|
entityToUpdateDto,
|
|
@@ -37,9 +63,20 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
37
63
|
}: TDescriptionsProps<Entity,
|
|
38
64
|
CreateDto,
|
|
39
65
|
UpdateDto,
|
|
40
|
-
TPathParams
|
|
66
|
+
TPathParams>,
|
|
67
|
+
ref: React.Ref<DescriptionsRefType>,
|
|
41
68
|
) => {
|
|
42
|
-
const
|
|
69
|
+
const { styles } = useStyles();
|
|
70
|
+
|
|
71
|
+
let [form] = useForm<Entity>();
|
|
72
|
+
if (!editable?.form) {
|
|
73
|
+
editable = {
|
|
74
|
+
...editable,
|
|
75
|
+
form,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
form = editable.form;
|
|
79
|
+
|
|
43
80
|
const actionRefComponent = useRef<ActionType>();
|
|
44
81
|
const actionRef = actionRefProp || actionRefComponent;
|
|
45
82
|
const intl = useIntl();
|
|
@@ -48,6 +85,84 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
48
85
|
|
|
49
86
|
const sections = columnsToDescriptionItemProps(columns, mainTitle);
|
|
50
87
|
|
|
88
|
+
const columnDataIndexToSection = sections.reduce((acc, section) => {
|
|
89
|
+
section.columns.forEach(column => {
|
|
90
|
+
if (Array.isArray(column.dataIndex)) {
|
|
91
|
+
throw new Error('We only support simple dataIndex for now');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
acc.set(column.dataIndex, section);
|
|
95
|
+
})
|
|
96
|
+
return acc;
|
|
97
|
+
}, new Map<Key, TDescriptionSection<Entity>>());
|
|
98
|
+
|
|
99
|
+
const {
|
|
100
|
+
contentViewModeButton,
|
|
101
|
+
contentViewMode
|
|
102
|
+
} = useContentViewMode({
|
|
103
|
+
mode: sections.length > 1 ? VIEW_MODE_TYPE.TABS : VIEW_MODE_TYPE.GENERAL
|
|
104
|
+
});
|
|
105
|
+
const [errorsPerSection, setErrorsPerSection] = useState<Map<TDescriptionSection<Entity>['key'], number>>(
|
|
106
|
+
new Map(sections.map(section => [section.key, 0]))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const handleSubmit = async () => {
|
|
110
|
+
try {
|
|
111
|
+
// Validate all fields in the form
|
|
112
|
+
const data = await form.validateFields();
|
|
113
|
+
// Let the parent component handle the submit logic
|
|
114
|
+
await onCreate(data);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Validation or submission failed:', error);
|
|
117
|
+
} finally {
|
|
118
|
+
// Recalculate the error count per section (tab) after validation
|
|
119
|
+
const newErrorsPerSection = new Map<string, number>();
|
|
120
|
+
sections.forEach((section) => {
|
|
121
|
+
let errorCount = 0;
|
|
122
|
+
section.columns.forEach((column) => {
|
|
123
|
+
if (form.getFieldError(column.dataIndex as NamePath<Entity>)?.length > 0) {
|
|
124
|
+
errorCount++;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
newErrorsPerSection.set(section.key, errorCount);
|
|
128
|
+
});
|
|
129
|
+
setErrorsPerSection(newErrorsPerSection);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
useImperativeHandle(ref, () => ({
|
|
134
|
+
reset: () => {
|
|
135
|
+
setErrorsPerSection(new Map(sections.map(section => [section.key, 0])));
|
|
136
|
+
form.resetFields();
|
|
137
|
+
},
|
|
138
|
+
submit: () => handleSubmit(),
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const onValuesChange = debounce((changedValues, allValues) => {
|
|
142
|
+
let key = Object.keys(changedValues)[0];
|
|
143
|
+
|
|
144
|
+
// changedValues = {} if we clear select value
|
|
145
|
+
if (!key) {
|
|
146
|
+
const previousValues = form.getFieldsValue(true);
|
|
147
|
+
key = Object.keys(previousValues).find((field) => !(field in allValues));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
form.validateFields([key])
|
|
151
|
+
.finally(() => {
|
|
152
|
+
const section = columnDataIndexToSection.get(key);
|
|
153
|
+
const dataIndexes = section.columns.map(column => {
|
|
154
|
+
return Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const errorsNumber = form.getFieldsError(dataIndexes as NamePath<Entity>[]).reduce((acc, field) => acc + field.errors.length, 0);
|
|
158
|
+
setErrorsPerSection((prev) => {
|
|
159
|
+
const updated = new Map(prev);
|
|
160
|
+
updated.set(section.key, errorsNumber);
|
|
161
|
+
return updated;
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}, 500);
|
|
165
|
+
|
|
51
166
|
const queryParams = useMemo(() => {
|
|
52
167
|
const join = params?.join;
|
|
53
168
|
const queryParams: TGetOneParams & TPathParams = {
|
|
@@ -65,6 +180,9 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
65
180
|
return queryParams;
|
|
66
181
|
}, [params, pathParams]);
|
|
67
182
|
|
|
183
|
+
const getKey = (index: number) =>
|
|
184
|
+
index + String(pathParams?.[idColumnName as keyof TPathParams])
|
|
185
|
+
|
|
68
186
|
const requestData = async () => {
|
|
69
187
|
if (!getOne) {
|
|
70
188
|
return;
|
|
@@ -90,9 +208,8 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
90
208
|
if (onUpdate && entityToUpdateDto) {
|
|
91
209
|
await onUpdate({
|
|
92
210
|
...queryParams,
|
|
93
|
-
...{} as
|
|
94
|
-
|
|
95
|
-
requestBody: entityToUpdateDto(pick(record, [propName])),
|
|
211
|
+
...{} as Partial<Entity>,
|
|
212
|
+
requestBody: entityToUpdateDto(pick(record, [propName as keyof Entity])),
|
|
96
213
|
});
|
|
97
214
|
}
|
|
98
215
|
|
|
@@ -111,7 +228,7 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
111
228
|
|
|
112
229
|
useEffect(() => {
|
|
113
230
|
setData(entity);
|
|
114
|
-
form.setFieldsValue(entity
|
|
231
|
+
form.setFieldsValue(entity);
|
|
115
232
|
}, [entity])
|
|
116
233
|
|
|
117
234
|
if (loading) {
|
|
@@ -129,38 +246,73 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
129
246
|
);
|
|
130
247
|
}
|
|
131
248
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
249
|
+
const formProps = contentViewMode === VIEW_MODE_TYPE.TABS ? {
|
|
250
|
+
onValuesChange,
|
|
251
|
+
} : undefined;
|
|
252
|
+
|
|
253
|
+
const contentViewSwitcher = sections.length > 1 ? contentViewModeButton : undefined;
|
|
254
|
+
const descriptions = sections.map((section, index) => {
|
|
255
|
+
return <ProDescriptions<Entity>
|
|
256
|
+
key={getKey(index)}
|
|
257
|
+
title={section.title as React.ReactNode}
|
|
258
|
+
actionRef={actionRef}
|
|
259
|
+
size={"small"}
|
|
260
|
+
bordered
|
|
261
|
+
loading={loading}
|
|
262
|
+
style={{ marginBottom: 20 }}
|
|
263
|
+
labelStyle={{ width: '15%' }}
|
|
264
|
+
dataSource={data as Entity}
|
|
265
|
+
className={styles.antDescriptionsStyles}
|
|
266
|
+
editable={canEdit ? {
|
|
267
|
+
type: 'multiple',
|
|
268
|
+
onSave,
|
|
269
|
+
deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
|
|
270
|
+
onlyAddOneLineAlertMessage: intl.formatMessage({ id: 'table.onlyAddOneLineAlertMessage' }),
|
|
271
|
+
cancelText: <Tooltip title={intl.formatMessage({ id: 'table.cancelText' })}><StopOutlined /></Tooltip>,
|
|
272
|
+
deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
|
|
273
|
+
saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
|
|
274
|
+
...editable,
|
|
275
|
+
} : undefined}
|
|
276
|
+
columns={section.columns}
|
|
277
|
+
extra={contentViewMode === VIEW_MODE_TYPE.GENERAL && index === 0 ? contentViewSwitcher : undefined}
|
|
278
|
+
formProps={formProps}
|
|
279
|
+
{...rest}
|
|
280
|
+
/>;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (contentViewMode === VIEW_MODE_TYPE.GENERAL) {
|
|
284
|
+
return descriptions;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const tabsItems: TabsProps['items'] = sections.map((section, index) => {
|
|
288
|
+
return {
|
|
289
|
+
key: getKey(index),
|
|
290
|
+
label: (
|
|
291
|
+
<Badge
|
|
292
|
+
size='small'
|
|
293
|
+
overflowCount={5}
|
|
294
|
+
count={errorsPerSection.get(section.key)}
|
|
295
|
+
>
|
|
296
|
+
{section.title as React.ReactNode}
|
|
297
|
+
</Badge>
|
|
298
|
+
),
|
|
299
|
+
forceRender: true,
|
|
300
|
+
children: descriptions[index],
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return <Tabs
|
|
305
|
+
defaultActiveKey="0"
|
|
306
|
+
items={tabsItems}
|
|
307
|
+
tabBarExtraContent={contentViewModeButton}
|
|
308
|
+
/>;
|
|
164
309
|
};
|
|
165
310
|
|
|
311
|
+
const Descriptions = React.forwardRef(DescriptionsComponent) as <Entity extends Record<string | symbol, any>,
|
|
312
|
+
CreateDto = Entity,
|
|
313
|
+
UpdateDto = Entity,
|
|
314
|
+
TPathParams = object>(
|
|
315
|
+
props: TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams>
|
|
316
|
+
) => React.ReactElement;
|
|
317
|
+
|
|
166
318
|
export default Descriptions;
|
|
@@ -12,7 +12,7 @@ const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
|
|
|
12
12
|
data,
|
|
13
13
|
onClose,
|
|
14
14
|
onSubmit,
|
|
15
|
-
|
|
15
|
+
...rest
|
|
16
16
|
}: TDescriptionsCreateModalProps<Entity>) => {
|
|
17
17
|
const sections = columnsToDescriptionItemProps(columns, 'General');
|
|
18
18
|
const [form] = useForm();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { MutableRefObject } from "react";
|
|
1
|
+
import React, { MutableRefObject } from "react";
|
|
2
2
|
import { ActionType } from "@ant-design/pro-table";
|
|
3
3
|
import { RowEditableConfig } from "@ant-design/pro-utils";
|
|
4
4
|
import { QueryJoin } from "@nestjsx/crud-request";
|
|
5
|
-
import { ProColumns
|
|
5
|
+
import { ProColumns } from "@ant-design/pro-components";
|
|
6
|
+
import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
|
|
6
7
|
|
|
7
8
|
export type TGetOneParams = {
|
|
8
9
|
/**
|
|
@@ -21,15 +22,25 @@ export type TGetOneParams = {
|
|
|
21
22
|
export type TDescriptionGetRequestParams = {
|
|
22
23
|
join?: QueryJoin | QueryJoin[];
|
|
23
24
|
};
|
|
25
|
+
|
|
26
|
+
export type DescriptionsRefType = {
|
|
27
|
+
reset: () => void;
|
|
28
|
+
submit: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
|
|
25
32
|
mainTitle?: ProColumns<Entity>['title'] | null,
|
|
26
33
|
entity?: Partial<Entity>,
|
|
27
34
|
getOne?: ({}: TGetOneParams & TPathParams) => Promise<Entity | null>,
|
|
28
|
-
onUpdate?: ({}:
|
|
29
|
-
|
|
35
|
+
onUpdate?: ({}: Partial<Entity> & {
|
|
36
|
+
requestBody: UpdateDto,
|
|
37
|
+
index?: number,
|
|
38
|
+
} & TPathParams) => Promise<Entity>,
|
|
39
|
+
onCreate: (data: Partial<Entity>) => Promise<void>;
|
|
40
|
+
onDelete?: ({}: Partial<Entity> & TPathParams) => Promise<void>,
|
|
30
41
|
pathParams?: TPathParams,
|
|
31
42
|
idColumnName?: string & keyof Entity,
|
|
32
|
-
entityToUpdateDto?: (entity:
|
|
43
|
+
entityToUpdateDto?: (entity: Entity) => UpdateDto,
|
|
33
44
|
createNewDefaultParams?: Partial<Entity>,
|
|
34
45
|
afterSave?: (record: Entity) => Promise<void>,
|
|
35
46
|
actionRef?: MutableRefObject<ActionType | undefined>,
|
|
@@ -38,9 +49,12 @@ export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = objec
|
|
|
38
49
|
params?: TDescriptionGetRequestParams,
|
|
39
50
|
columns: ProColumns<Entity>[],
|
|
40
51
|
onEntityChange?: (entity: Entity | null) => void;
|
|
41
|
-
|
|
52
|
+
ref?: React.Ref<DescriptionsRefType>,
|
|
53
|
+
} & Omit<ProDescriptionsProps<Entity>, 'columns'>;
|
|
42
54
|
|
|
43
55
|
export type TDescriptionsCreateModalProps<Entity> = Omit<ProDescriptionsProps<Entity>, 'columns'> & {
|
|
56
|
+
modalTitle?: string,
|
|
57
|
+
mainTitle?: ProColumns<Entity>['title'] | null,
|
|
44
58
|
idColumnName: string & keyof Entity | (string & keyof Entity)[],
|
|
45
59
|
columns: ProColumns<Entity>[],
|
|
46
60
|
data: Partial<Entity> | undefined,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Button, Tooltip } from "antd";
|
|
2
|
+
import { AppstoreOutlined, MenuOutlined } from "@ant-design/icons";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export enum VIEW_MODE_TYPE {
|
|
6
|
+
TABS = 'tabs',
|
|
7
|
+
GENERAL = 'general'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function useContentViewMode({
|
|
11
|
+
mode,
|
|
12
|
+
}: {
|
|
13
|
+
mode?: VIEW_MODE_TYPE;
|
|
14
|
+
} = {}) {
|
|
15
|
+
const [contentViewMode, setContentViewMode] = useState<VIEW_MODE_TYPE>(mode || VIEW_MODE_TYPE.GENERAL);
|
|
16
|
+
|
|
17
|
+
const contentViewModeButton = <Tooltip
|
|
18
|
+
title={contentViewMode === VIEW_MODE_TYPE.TABS ? 'Switch to general view' : 'Switch to tabs view'}
|
|
19
|
+
key="viewModeToggle">
|
|
20
|
+
<Button
|
|
21
|
+
type="text"
|
|
22
|
+
icon={contentViewMode === VIEW_MODE_TYPE.TABS ? <MenuOutlined /> : <AppstoreOutlined />}
|
|
23
|
+
onClick={() => setContentViewMode(contentViewMode === VIEW_MODE_TYPE.TABS ? VIEW_MODE_TYPE.GENERAL : VIEW_MODE_TYPE.TABS)}
|
|
24
|
+
/>
|
|
25
|
+
</Tooltip>;
|
|
26
|
+
|
|
27
|
+
return { contentViewMode, contentViewModeButton };
|
|
28
|
+
}
|
|
@@ -3,15 +3,19 @@ import { ProDescriptionsItemProps } from "@ant-design/pro-descriptions";
|
|
|
3
3
|
|
|
4
4
|
export type TDescriptionSection<T> = {
|
|
5
5
|
title: ProColumns<T>['title'] | null;
|
|
6
|
+
// 'general' is a special value for the main section including only columns without children
|
|
7
|
+
key: string | 'general';
|
|
6
8
|
columns: ProDescriptionsItemProps<T>[];
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function columnsToDescriptionItemProps<T>(
|
|
10
12
|
columns: ProColumns<T>[],
|
|
11
13
|
mainTitle: ProColumns<T>['title'] | null = null,
|
|
14
|
+
key: string | 'general' = 'general'
|
|
12
15
|
): TDescriptionSection<T>[] {
|
|
13
16
|
const baseSection: TDescriptionSection<T> = {
|
|
14
17
|
title: mainTitle,
|
|
18
|
+
key,
|
|
15
19
|
columns: [],
|
|
16
20
|
}
|
|
17
21
|
const result: TDescriptionSection<T>[] = [baseSection];
|
|
@@ -22,7 +26,8 @@ export function columnsToDescriptionItemProps<T>(
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
if (column.children) {
|
|
25
|
-
|
|
29
|
+
const dataIndex = String(Array.isArray(column.dataIndex) ? column.dataIndex[0] : column.dataIndex);
|
|
30
|
+
result.push(...columnsToDescriptionItemProps(column.children, column.title, dataIndex));
|
|
26
31
|
} else {
|
|
27
32
|
const {
|
|
28
33
|
children,
|
|
@@ -14,7 +14,7 @@ const useStyles = createStyles(() => {
|
|
|
14
14
|
}
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
const BulkDeleteButton = <Entity extends Record<string | symbol, any
|
|
17
|
+
const BulkDeleteButton = <Entity extends Record<string | symbol, any>, TPathParams>(
|
|
18
18
|
{
|
|
19
19
|
selectedRecords,
|
|
20
20
|
lastRequest,
|
|
@@ -23,7 +23,7 @@ const BulkDeleteButton = <Entity extends Record<string | symbol, any>>(
|
|
|
23
23
|
} : {
|
|
24
24
|
selectedRecords: Entity[],
|
|
25
25
|
allSelected: boolean,
|
|
26
|
-
lastRequest: [TGetAllParams &
|
|
26
|
+
lastRequest: [TGetAllParams & TPathParams, any] | [],
|
|
27
27
|
onDelete: () => Promise<void>
|
|
28
28
|
}) => {
|
|
29
29
|
const { styles } = useStyles();
|
|
@@ -117,7 +117,7 @@ const BulkEditDialog = <Entity extends Record<string | symbol, any>>(
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const BulkEditButton = <Entity extends Record<string | symbol, any
|
|
120
|
+
const BulkEditButton = <Entity extends Record<string | symbol, any>, TPathParams>(
|
|
121
121
|
{
|
|
122
122
|
selectedRecords,
|
|
123
123
|
lastRequest,
|
|
@@ -127,7 +127,7 @@ const BulkEditButton = <Entity extends Record<string | symbol, any>>(
|
|
|
127
127
|
onSubmit,
|
|
128
128
|
} : {
|
|
129
129
|
selectedRecords: Entity[],
|
|
130
|
-
lastRequest: [TGetAllParams &
|
|
130
|
+
lastRequest: [TGetAllParams & TPathParams, any] | [],
|
|
131
131
|
idColumnName: string & keyof Entity | (string & keyof Entity)[],
|
|
132
132
|
allSelected: boolean,
|
|
133
133
|
columns: ProColumns<Entity>[],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ProColumns } from "@ant-design/pro-components";
|
|
2
|
+
import { Button, Modal } from "antd";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { Descriptions, DescriptionsRefType } from "../Descriptions";
|
|
5
|
+
import { buildFieldsFromColumnsForDescriptionsDisplay } from "./tableTools";
|
|
6
|
+
|
|
7
|
+
export interface CreateEntityModalProps<Entity> {
|
|
8
|
+
/** Whether the modal is visible */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** The entity (or partial entity) data to edit */
|
|
11
|
+
entity: Partial<Entity> | undefined;
|
|
12
|
+
/** Modal title */
|
|
13
|
+
title: string;
|
|
14
|
+
/** Main title for the Descriptions component */
|
|
15
|
+
mainTitle?: ProColumns<Entity>['title'] | null;
|
|
16
|
+
/** Table columns used to render the fields */
|
|
17
|
+
columns: any[];
|
|
18
|
+
/** Column key (or keys) used as an ID */
|
|
19
|
+
idColumnName: string | string[];
|
|
20
|
+
/** Called when the modal is cancelled (closed) */
|
|
21
|
+
onCancel: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Called when the form is submitted.
|
|
24
|
+
* Receives the validated form data.
|
|
25
|
+
*/
|
|
26
|
+
onSubmit: (data: any) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CreateEntityModal<
|
|
30
|
+
Entity,
|
|
31
|
+
CreateDto = Entity,
|
|
32
|
+
UpdateDto = Entity,
|
|
33
|
+
TPathParams = object
|
|
34
|
+
>({
|
|
35
|
+
entity,
|
|
36
|
+
open = entity !== undefined,
|
|
37
|
+
title,
|
|
38
|
+
mainTitle,
|
|
39
|
+
columns,
|
|
40
|
+
idColumnName,
|
|
41
|
+
onCancel,
|
|
42
|
+
onSubmit,
|
|
43
|
+
}: CreateEntityModalProps<Entity>) {
|
|
44
|
+
const descriptionsRef = useRef<DescriptionsRefType>(null);
|
|
45
|
+
|
|
46
|
+
// Calculate the editable keys from the columns and idColumnName
|
|
47
|
+
const editableKeys = [...buildFieldsFromColumnsForDescriptionsDisplay(columns, idColumnName)];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Modal
|
|
51
|
+
title={title}
|
|
52
|
+
open={open}
|
|
53
|
+
width="80%"
|
|
54
|
+
closeIcon={true}
|
|
55
|
+
footer={[
|
|
56
|
+
<Button key="submit" type="primary" onClick={() => descriptionsRef.current?.submit()}>
|
|
57
|
+
Create
|
|
58
|
+
</Button>,
|
|
59
|
+
]}
|
|
60
|
+
onCancel={() => {
|
|
61
|
+
descriptionsRef.current?.reset();
|
|
62
|
+
onCancel();
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<Descriptions<Entity, CreateDto, UpdateDto, TPathParams>
|
|
66
|
+
ref={descriptionsRef}
|
|
67
|
+
mainTitle={mainTitle}
|
|
68
|
+
columns={columns ?? []}
|
|
69
|
+
entity={entity}
|
|
70
|
+
size="small"
|
|
71
|
+
bordered
|
|
72
|
+
column={2}
|
|
73
|
+
style={{ marginBottom: 20 }}
|
|
74
|
+
labelStyle={{ width: '15%' }}
|
|
75
|
+
contentStyle={{ width: '25%' }}
|
|
76
|
+
canEdit={true}
|
|
77
|
+
onCreate={onSubmit}
|
|
78
|
+
editable={{
|
|
79
|
+
editableKeys,
|
|
80
|
+
actionRender: () => [],
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
</Modal>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Popconfirm, Tooltip } from "antd";
|
|
2
|
+
import { DeleteOutlined, LoadingOutlined } from "@ant-design/icons";
|
|
3
|
+
import { createStyles } from "antd-style";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { useIntl } from "react-intl";
|
|
6
|
+
|
|
7
|
+
const useStyles = createStyles(() => {
|
|
8
|
+
return {
|
|
9
|
+
popconfirm: {
|
|
10
|
+
'.ant-popconfirm-description': {
|
|
11
|
+
marginTop: '0 !important',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const DeleteButton = (
|
|
18
|
+
{
|
|
19
|
+
onDelete,
|
|
20
|
+
}: {
|
|
21
|
+
onDelete: () => Promise<void>
|
|
22
|
+
}) => {
|
|
23
|
+
const {styles} = useStyles();
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const intl = useIntl();
|
|
26
|
+
|
|
27
|
+
return (<a key="deleteButton">
|
|
28
|
+
{!loading && <Popconfirm
|
|
29
|
+
overlayClassName={styles.popconfirm}
|
|
30
|
+
title={false}
|
|
31
|
+
description={intl.formatMessage({id: "table.deletePopconfirmMessage"})}
|
|
32
|
+
onConfirm={() => {
|
|
33
|
+
setLoading(true);
|
|
34
|
+
onDelete().finally(() => setLoading(false));
|
|
35
|
+
}}>
|
|
36
|
+
<Tooltip title={intl.formatMessage({id: "table.deleteText"})}>
|
|
37
|
+
<DeleteOutlined/>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
</Popconfirm>}
|
|
40
|
+
{loading && <LoadingOutlined/>}
|
|
41
|
+
</a>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
export default DeleteButton;
|