@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boarteam/boar-pack-common-frontend",
3
- "version": "2.4.0",
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": "29f4f6c6598c7422bef8e02dfd1e679b471a4aa4"
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 Descriptions = <Entity extends Record<string | symbol, any>,
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> & Omit<ProDescriptionsProps<Entity>, 'columns'>
66
+ TPathParams>,
67
+ ref: React.Ref<DescriptionsRefType>,
41
68
  ) => {
42
- const [form] = useForm<Entity>();
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 Record<keyof Entity, string>, // todo: fix this
94
- // @ts-ignore
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 as 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
- 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
- );
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
- ...rest
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, ProDescriptionsProps } from "@ant-design/pro-components";
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?: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>,
29
- onDelete?: ({}: Record<keyof Entity, string> & TPathParams) => Promise<void>,
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: Partial<Entity>) => UpdateDto,
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
- result.push(...columnsToDescriptionItemProps(column.children, column.title));
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 & Record<string, string | number>, any] | [],
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 & Record<string, string | number>, any] | [],
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;