@boarteam/boar-pack-common-frontend 2.4.1 → 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.1",
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": "15bb55f9ca20f3a86c95c9b519e3b31ef1ed1772"
49
+ "gitHead": "8eb798bf7672983cd13372098b1cfae43d73d190"
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 "../Table/ContentViewModeButton";
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 Descriptions = <Entity extends Record<string | symbol, any>,
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> & Omit<ProDescriptionsProps<Entity>, 'columns'>
66
+ TPathParams>,
67
+ ref: React.Ref<DescriptionsRefType>,
63
68
  ) => {
64
69
  const { styles } = useStyles();
65
- const [form] = useForm<Entity>();
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 Record<keyof Entity, string>, // todo: fix this
120
- // @ts-ignore
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 as 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 descriptions = sections.map((section, index) => {
159
- // In the general view mode we need to render extra elements only ones for the top one section
160
- if (viewMode === VIEW_MODE_TYPE.GENERAL && rest.extra && index !== 0) {
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
- return (
193
- <>
194
- {
195
- viewMode === VIEW_MODE_TYPE.TABS ?
196
- (<Tabs defaultActiveKey="0">
197
- {
198
- descriptions.map((description, index) => (
199
- <Tabs.TabPane
200
- tab={description.props.title as React.ReactNode}
201
- key={getKey(index)}
202
- >
203
- {description}
204
- </Tabs.TabPane>
205
- ))
206
- }
207
- </Tabs>)
208
- : descriptions
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 React, { useEffect, useMemo } from "react";
2
- import { Button, Modal, Tabs } from "antd";
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
- viewMode = VIEW_MODE_TYPE.GENERAL,
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
- viewMode === VIEW_MODE_TYPE.TABS ?
75
- (<Tabs defaultActiveKey="0">
76
- {
77
- descriptions.map((description, index) => (
78
- <Tabs.TabPane
79
- tab={description.props.title as React.ReactNode}
80
- key={getKey(index)}
81
- >
82
- {description}
83
- </Tabs.TabPane>
84
- ))
85
- }
86
- </Tabs>)
87
- : descriptions
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?: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>,
31
- 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>,
32
41
  pathParams?: TPathParams,
33
42
  idColumnName?: string & keyof Entity,
34
- entityToUpdateDto?: (entity: Partial<Entity>) => UpdateDto,
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
- viewMode?: VIEW_MODE_TYPE,
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
- 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
+ }