@boarteam/boar-pack-common-frontend 2.4.1 → 2.5.1-alpha.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 +151 -47
- package/src/components/Descriptions/DescriptionsCreateModal.tsx +22 -50
- package/src/components/Descriptions/descriptionTypes.ts +18 -8
- 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 +88 -292
- 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 +14 -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/src/components/Table/ContentViewModeButton.tsx +0 -27
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.1-alpha.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": "fe24b636ace6e71cc60a3f65126f8520f926aba1"
|
|
50
50
|
}
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { ActionType } from "@ant-design/pro-table";
|
|
2
|
-
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Button, Result, Tabs, 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";
|
|
15
|
-
import { VIEW_MODE_TYPE } from "
|
|
14
|
+
import useContentViewMode, { VIEW_MODE_TYPE } from "./useContentViewMode";
|
|
16
15
|
import { createStyles } from "antd-style";
|
|
16
|
+
import { debounce } from "lodash";
|
|
17
|
+
import { NamePath } from "antd/lib/form/interface";
|
|
17
18
|
|
|
18
19
|
const useStyles = createStyles(() => {
|
|
19
20
|
return {
|
|
21
|
+
/**
|
|
22
|
+
* Styles for the ant-descriptions component to show edit icon on hover
|
|
23
|
+
*/
|
|
20
24
|
antDescriptionsStyles: {
|
|
21
25
|
'.anticon-edit': {
|
|
22
26
|
opacity: 0,
|
|
@@ -34,16 +38,17 @@ const useStyles = createStyles(() => {
|
|
|
34
38
|
}
|
|
35
39
|
})
|
|
36
40
|
|
|
37
|
-
const
|
|
41
|
+
const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
|
|
38
42
|
CreateDto = Entity,
|
|
39
43
|
UpdateDto = Entity,
|
|
40
44
|
TPathParams = object,
|
|
41
|
-
|
|
45
|
+
>(
|
|
42
46
|
{
|
|
43
|
-
mainTitle,
|
|
47
|
+
mainTitle = 'General',
|
|
44
48
|
entity,
|
|
45
49
|
getOne,
|
|
46
50
|
onUpdate,
|
|
51
|
+
onCreate,
|
|
47
52
|
pathParams,
|
|
48
53
|
idColumnName = 'id',
|
|
49
54
|
entityToUpdateDto,
|
|
@@ -54,15 +59,24 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
54
59
|
columns,
|
|
55
60
|
params,
|
|
56
61
|
onEntityChange,
|
|
57
|
-
viewMode = VIEW_MODE_TYPE.GENERAL,
|
|
58
62
|
...rest
|
|
59
63
|
}: TDescriptionsProps<Entity,
|
|
60
64
|
CreateDto,
|
|
61
65
|
UpdateDto,
|
|
62
|
-
TPathParams
|
|
66
|
+
TPathParams>,
|
|
67
|
+
ref: React.Ref<DescriptionsRefType>,
|
|
63
68
|
) => {
|
|
64
69
|
const { styles } = useStyles();
|
|
65
|
-
|
|
70
|
+
|
|
71
|
+
let [form] = useForm<Entity>();
|
|
72
|
+
if (!editable?.form) {
|
|
73
|
+
editable = {
|
|
74
|
+
...editable,
|
|
75
|
+
form,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
form = editable.form;
|
|
79
|
+
|
|
66
80
|
const actionRefComponent = useRef<ActionType>();
|
|
67
81
|
const actionRef = actionRefProp || actionRefComponent;
|
|
68
82
|
const intl = useIntl();
|
|
@@ -71,6 +85,84 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
71
85
|
|
|
72
86
|
const sections = columnsToDescriptionItemProps(columns, mainTitle);
|
|
73
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
|
+
|
|
74
166
|
const queryParams = useMemo(() => {
|
|
75
167
|
const join = params?.join;
|
|
76
168
|
const queryParams: TGetOneParams & TPathParams = {
|
|
@@ -116,9 +208,8 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
116
208
|
if (onUpdate && entityToUpdateDto) {
|
|
117
209
|
await onUpdate({
|
|
118
210
|
...queryParams,
|
|
119
|
-
...{} as
|
|
120
|
-
|
|
121
|
-
requestBody: entityToUpdateDto(pick(record, [propName])),
|
|
211
|
+
...{} as Partial<Entity>,
|
|
212
|
+
requestBody: entityToUpdateDto(pick(record, [propName as keyof Entity])),
|
|
122
213
|
});
|
|
123
214
|
}
|
|
124
215
|
|
|
@@ -137,7 +228,7 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
137
228
|
|
|
138
229
|
useEffect(() => {
|
|
139
230
|
setData(entity);
|
|
140
|
-
form.setFieldsValue(entity
|
|
231
|
+
form.setFieldsValue(entity);
|
|
141
232
|
}, [entity])
|
|
142
233
|
|
|
143
234
|
if (loading) {
|
|
@@ -155,15 +246,13 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
155
246
|
);
|
|
156
247
|
}
|
|
157
248
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
rest.extra = null;
|
|
162
|
-
}
|
|
249
|
+
const formProps = contentViewMode === VIEW_MODE_TYPE.TABS ? {
|
|
250
|
+
onValuesChange,
|
|
251
|
+
} : undefined;
|
|
163
252
|
|
|
253
|
+
const contentViewSwitcher = sections.length > 1 ? contentViewModeButton : undefined;
|
|
254
|
+
const descriptions = sections.map((section, index) => {
|
|
164
255
|
return <ProDescriptions<Entity>
|
|
165
|
-
// @ts-ignore-next-line
|
|
166
|
-
form={form}
|
|
167
256
|
key={getKey(index)}
|
|
168
257
|
title={section.title as React.ReactNode}
|
|
169
258
|
actionRef={actionRef}
|
|
@@ -183,32 +272,47 @@ const Descriptions = <Entity extends Record<string | symbol, any>,
|
|
|
183
272
|
deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
|
|
184
273
|
saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
|
|
185
274
|
...editable,
|
|
186
|
-
}: undefined}
|
|
275
|
+
} : undefined}
|
|
187
276
|
columns={section.columns}
|
|
277
|
+
extra={contentViewMode === VIEW_MODE_TYPE.GENERAL && index === 0 ? contentViewSwitcher : undefined}
|
|
278
|
+
formProps={formProps}
|
|
188
279
|
{...rest}
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
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
|
+
/>;
|
|
212
309
|
};
|
|
213
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
|
+
|
|
214
318
|
export default Descriptions;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Button, Modal
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
import { Button, Modal } from "antd";
|
|
3
3
|
import { TDescriptionsCreateModalProps } from "./descriptionTypes";
|
|
4
4
|
import { ProDescriptions } from "@ant-design/pro-components";
|
|
5
5
|
import { columnsToDescriptionItemProps } from "./useDescriptionColumns";
|
|
6
6
|
import { useForm } from "antd/es/form/Form";
|
|
7
7
|
import { buildFieldsFromColumnsForDescriptionsDisplay } from "../Table";
|
|
8
|
-
import { VIEW_MODE_TYPE } from "../Table/ContentViewModeButton";
|
|
9
8
|
|
|
10
9
|
const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
|
|
11
10
|
idColumnName,
|
|
@@ -13,15 +12,11 @@ const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
|
|
|
13
12
|
data,
|
|
14
13
|
onClose,
|
|
15
14
|
onSubmit,
|
|
16
|
-
|
|
17
|
-
...rest
|
|
15
|
+
...rest
|
|
18
16
|
}: TDescriptionsCreateModalProps<Entity>) => {
|
|
19
17
|
const sections = columnsToDescriptionItemProps(columns, 'General');
|
|
20
18
|
const [form] = useForm();
|
|
21
19
|
|
|
22
|
-
const getKey = (index: number) =>
|
|
23
|
-
index + (Array.isArray(idColumnName) ? idColumnName.join('-') : idColumnName)
|
|
24
|
-
|
|
25
20
|
const editableKeys = useMemo(() => {
|
|
26
21
|
return [
|
|
27
22
|
...buildFieldsFromColumnsForDescriptionsDisplay(
|
|
@@ -35,31 +30,6 @@ const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
|
|
|
35
30
|
data ? form.setFieldsValue(data) : form.resetFields();
|
|
36
31
|
}, [data]);
|
|
37
32
|
|
|
38
|
-
const descriptions = sections.map((section, index) => {
|
|
39
|
-
// In the general view mode we need to render extra elements only ones for the top one section
|
|
40
|
-
if (viewMode === VIEW_MODE_TYPE.GENERAL && rest.extra && index !== 0) {
|
|
41
|
-
rest.extra = null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return <ProDescriptions<Entity>
|
|
45
|
-
key={getKey(index)}
|
|
46
|
-
title={section.title as React.ReactNode}
|
|
47
|
-
size={"small"}
|
|
48
|
-
bordered
|
|
49
|
-
column={2}
|
|
50
|
-
style={{ marginBottom: 20 }}
|
|
51
|
-
labelStyle={{ width: '15%' }}
|
|
52
|
-
contentStyle={{ width: '25%' }}
|
|
53
|
-
editable={{
|
|
54
|
-
form,
|
|
55
|
-
editableKeys,
|
|
56
|
-
actionRender: () => [],
|
|
57
|
-
}}
|
|
58
|
-
columns={section.columns}
|
|
59
|
-
{...rest}
|
|
60
|
-
/>
|
|
61
|
-
})
|
|
62
|
-
|
|
63
33
|
return (
|
|
64
34
|
<Modal
|
|
65
35
|
open={data !== undefined}
|
|
@@ -68,24 +38,26 @@ const DescriptionsCreateModal = <Entity extends Record<string | symbol, any>>({
|
|
|
68
38
|
footer={[
|
|
69
39
|
<Button key='submit' type="primary" onClick={async () => form.validateFields().then(onSubmit)}>Create</Button>
|
|
70
40
|
]}
|
|
71
|
-
closeIcon={false}
|
|
72
41
|
>
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
))}
|
|
89
61
|
</Modal>
|
|
90
62
|
);
|
|
91
63
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
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
5
|
import { ProColumns } from "@ant-design/pro-components";
|
|
6
|
-
import { VIEW_MODE_TYPE } from "../Table/ContentViewModeButton";
|
|
7
6
|
import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
|
|
8
7
|
|
|
9
8
|
export type TGetOneParams = {
|
|
@@ -23,15 +22,25 @@ export type TGetOneParams = {
|
|
|
23
22
|
export type TDescriptionGetRequestParams = {
|
|
24
23
|
join?: QueryJoin | QueryJoin[];
|
|
25
24
|
};
|
|
25
|
+
|
|
26
|
+
export type DescriptionsRefType = {
|
|
27
|
+
reset: () => void;
|
|
28
|
+
submit: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
26
31
|
export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
|
|
27
32
|
mainTitle?: ProColumns<Entity>['title'] | null,
|
|
28
33
|
entity?: Partial<Entity>,
|
|
29
34
|
getOne?: ({}: TGetOneParams & TPathParams) => Promise<Entity | null>,
|
|
30
|
-
onUpdate?: ({}:
|
|
31
|
-
|
|
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>,
|
|
32
41
|
pathParams?: TPathParams,
|
|
33
42
|
idColumnName?: string & keyof Entity,
|
|
34
|
-
entityToUpdateDto?: (entity:
|
|
43
|
+
entityToUpdateDto?: (entity: Entity) => UpdateDto,
|
|
35
44
|
createNewDefaultParams?: Partial<Entity>,
|
|
36
45
|
afterSave?: (record: Entity) => Promise<void>,
|
|
37
46
|
actionRef?: MutableRefObject<ActionType | undefined>,
|
|
@@ -40,14 +49,15 @@ export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = objec
|
|
|
40
49
|
params?: TDescriptionGetRequestParams,
|
|
41
50
|
columns: ProColumns<Entity>[],
|
|
42
51
|
onEntityChange?: (entity: Entity | null) => void;
|
|
43
|
-
|
|
44
|
-
}
|
|
52
|
+
ref?: React.Ref<DescriptionsRefType>,
|
|
53
|
+
} & Omit<ProDescriptionsProps<Entity>, 'columns'>;
|
|
45
54
|
|
|
46
55
|
export type TDescriptionsCreateModalProps<Entity> = Omit<ProDescriptionsProps<Entity>, 'columns'> & {
|
|
56
|
+
modalTitle?: string,
|
|
57
|
+
mainTitle?: ProColumns<Entity>['title'] | null,
|
|
47
58
|
idColumnName: string & keyof Entity | (string & keyof Entity)[],
|
|
48
59
|
columns: ProColumns<Entity>[],
|
|
49
60
|
data: Partial<Entity> | undefined,
|
|
50
61
|
onSubmit: (data: Entity) => Promise<void>,
|
|
51
62
|
onClose: () => void,
|
|
52
|
-
viewMode?: VIEW_MODE_TYPE,
|
|
53
63
|
}
|
|
@@ -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
|
+
}
|