@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.
@@ -1,30 +1,14 @@
1
1
  import ProTable, { ActionType } from "@ant-design/pro-table";
2
- import React, { useEffect, useRef, useState } from "react";
3
- import { Button, Popover, Space, Tooltip, message } from "antd";
4
- import { DeleteOutlined, PlusOutlined, QuestionCircleTwoTone, StopOutlined } from "@ant-design/icons";
5
- import { FormattedMessage, useIntl } from "react-intl";
6
- import { flushSync } from "react-dom";
7
- import { applyKeywordToSearch, buildJoinFields, collectFieldsFromColumns, getFiltersSearch } from "./tableTools";
8
- import { TFilterParams, TFilters, TGetAllParams, TSort, TTableProps } from "./tableTypes";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Modal } from "antd";
4
+ import { TFilterParams, TFilters, TSort, TTableProps } from "./tableTypes";
9
5
  import useColumnsSets from "./useColumnsSets";
10
- import DescriptionsCreateModal from "../Descriptions/DescriptionsCreateModal";
11
- import BulkEditButton from "./BulkEditButton";
12
- import _ from "lodash";
13
- import BulkDeleteButton from "./BulkDeleteButton";
14
6
  import { createStyles } from "antd-style";
15
-
16
- let creatingRecordsCount = 0;
17
-
18
- export const KEY_SYMBOL = Symbol('key');
19
- const NEW_RECORD = 'NEW_RECORD';
20
-
21
- export function getNewId(): string {
22
- return NEW_RECORD + creatingRecordsCount++;
23
- }
24
-
25
- export function isRecordNew(record: Record<string | symbol, any>): boolean {
26
- return record[KEY_SYMBOL]?.startsWith?.(NEW_RECORD) || record.id?.startsWith?.(NEW_RECORD) || false;
27
- }
7
+ import { Descriptions } from "../Descriptions";
8
+ import { KEY_SYMBOL, useCreation } from "./useCreation";
9
+ import { getTableDataQueryParams } from "./getTableDataQueryParams";
10
+ import { useEditableTable } from "./useEditableTable";
11
+ import { useBulkEditing } from "./useBulkEditing";
28
12
 
29
13
  const useStyles = createStyles(() => {
30
14
  return {
@@ -42,7 +26,7 @@ const Table = <Entity extends Record<string | symbol, any>,
42
26
  TEntityParams = {},
43
27
  TPathParams extends Record<string, string | number> = {},
44
28
  TKey = string,
45
- >(
29
+ >(
46
30
  {
47
31
  getAll,
48
32
  onCreate,
@@ -55,6 +39,7 @@ const Table = <Entity extends Record<string | symbol, any>,
55
39
  entityToCreateDto,
56
40
  entityToUpdateDto,
57
41
  createNewDefaultParams,
42
+ editableRecord,
58
43
  afterSave,
59
44
  actionRef: actionRefProp,
60
45
  editable,
@@ -68,7 +53,9 @@ const Table = <Entity extends Record<string | symbol, any>,
68
53
  popupCreation = false,
69
54
  toolBarRender,
70
55
  params,
71
- popupDataState,
56
+ editPopupTitle,
57
+ createPopupTitle,
58
+ descriptionsMainTitle,
72
59
  ...rest
73
60
  }: TTableProps<Entity,
74
61
  CreateDto,
@@ -78,19 +65,63 @@ const Table = <Entity extends Record<string | symbol, any>,
78
65
  ) => {
79
66
  const actionRefComponent = useRef<ActionType>();
80
67
  const actionRef = actionRefProp || actionRefComponent;
81
- const [createPopupData, setCreatePopupData] = popupDataState ?? useState<Partial<Entity> | undefined>();
82
- const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
83
- const [selectedRecords, setSelectedRecords] = useState<Entity[]>([]);
84
- const [lastRequest, setLastRequest] = useState<[TGetAllParams & TPathParams, any] | []>([]);
85
- const [allSelected, setAllSelected] = useState(false);
68
+ const [updatePopupData, setUpdatePopupData] = useState<Partial<Entity> | undefined>();
86
69
  const { styles } = useStyles();
87
- const [messageApi, contextHolder] = message.useMessage();
88
70
 
89
- const intl = useIntl();
71
+ const {
72
+ editableConfig,
73
+ } = useEditableTable<Entity, CreateDto, UpdateDto, TPathParams>({
74
+ actionRef,
75
+ pathParams,
76
+ onCreate,
77
+ onUpdate,
78
+ onDelete,
79
+ entityToCreateDto,
80
+ entityToUpdateDto,
81
+ afterSave,
82
+ editable,
83
+ onDeleteMany,
84
+ onUpdateMany,
85
+ });
86
+
87
+ const {
88
+ rowSelection,
89
+ setSelectedRecords,
90
+ setLastRequest,
91
+ bulkEditButton,
92
+ bulkDeleteButton,
93
+ messagesContext,
94
+ } = useBulkEditing<Entity, TPathParams, UpdateDto, TEntityParams & TFilterParams>({
95
+ actionRef,
96
+ columns,
97
+ idColumnName,
98
+ onDeleteMany,
99
+ onUpdateMany,
100
+ entityToUpdateDto,
101
+ pathParams,
102
+ });
103
+
104
+ const {
105
+ creationModal,
106
+ createButton,
107
+ } = useCreation<Entity, CreateDto, TPathParams>({
108
+ title: createPopupTitle,
109
+ mainTitle: descriptionsMainTitle,
110
+ columns: columns,
111
+ idColumnName: idColumnName,
112
+ onCreate,
113
+ pathParams,
114
+ entityToCreateDto,
115
+ actionRef,
116
+ createButtonSize: rest.size,
117
+ popupCreation,
118
+ createNewDefaultParams,
119
+ });
90
120
 
91
121
  useEffect(() => {
122
+ setUpdatePopupData(editableRecord);
92
123
  actionRef?.current?.reload();
93
- }, [JSON.stringify(pathParams), JSON.stringify(params)]);
124
+ }, [editableRecord, JSON.stringify(pathParams), JSON.stringify(params)]);
94
125
 
95
126
  const {
96
127
  columnsSetSelect: localColumnsSetSelect,
@@ -108,55 +139,17 @@ const Table = <Entity extends Record<string | symbol, any>,
108
139
  sort: TSort = {},
109
140
  filters: TFilters = {},
110
141
  ) => {
111
- const {
112
- current,
113
- pageSize,
114
- keyword,
115
- baseFilters,
116
- join,
117
- sortMap,
118
- ...filtersFromSearchForm
119
- } = params;
120
-
121
- const queryParams: TGetAllParams & TPathParams = {
122
- ...pathParams,
123
- page: current,
124
- limit: pageSize,
125
- };
126
-
127
- const sortBy = Object
128
- .entries(sort)
129
- .reduce<string[]>(
130
- (data: string[], [key, direction]) => {
131
- data.push(`${sortMap?.[key] || key},${direction === 'ascend' ? 'ASC' : 'DESC'}`);
132
- return data;
133
- },
134
- []
135
- );
136
- if (!sortBy.length && defaultSort) {
137
- sortBy.push(defaultSort.join(','));
138
- }
139
- queryParams.sort = sortBy;
140
-
141
- let search = getFiltersSearch({
142
- baseFilters,
143
- filters: {
144
- ...filters,
145
- ...filtersFromSearchForm,
146
- },
142
+ const queryParams = getTableDataQueryParams({
143
+ params,
144
+ sort,
145
+ filters,
146
+ pathParams,
147
+ defaultSort,
147
148
  searchableColumns,
148
- });
149
- search = applyKeywordToSearch(search, searchableColumns!, columnsState.value!, keyword);
150
- queryParams.s = JSON.stringify(search);
151
-
152
- const { joinSelect, joinFields } = buildJoinFields(join);
153
- queryParams.join = joinSelect;
154
-
155
- queryParams.fields = columns && collectFieldsFromColumns(
156
149
  columns,
157
150
  idColumnName,
158
- joinFields,
159
- ) || [];
151
+ columnsState,
152
+ });
160
153
 
161
154
  const result = await getAll(queryParams);
162
155
 
@@ -168,26 +161,6 @@ const Table = <Entity extends Record<string | symbol, any>,
168
161
  return result;
169
162
  }
170
163
 
171
- const createButton = <Button
172
- size={rest.size}
173
- type="primary"
174
- key="create"
175
- onClick={() => {
176
- if (popupCreation) {
177
- setCreatePopupData(createNewDefaultParams);
178
- } else {
179
- actionRef?.current?.addEditRecord({
180
- [KEY_SYMBOL]: getNewId(),
181
- ...createNewDefaultParams,
182
- }, {
183
- position: 'top',
184
- });
185
- }
186
- }}
187
- >
188
- <PlusOutlined /> <FormattedMessage id={'table.newButton'} />
189
- </Button>;
190
-
191
164
  return (<>
192
165
  <ProTable<Entity, TEntityParams & TFilterParams>
193
166
  actionRef={actionRef}
@@ -210,119 +183,17 @@ const Table = <Entity extends Record<string | symbol, any>,
210
183
  }}
211
184
  bordered
212
185
  search={false}
213
- editable={{
214
- type: 'multiple',
215
- editableKeys,
216
- onChange: setEditableRowKeys,
217
- async onSave(
218
- id,
219
- record,
220
- origin,
221
- newLine,
222
- ) {
223
- if (newLine) {
224
- await onCreate?.({
225
- ...pathParams,
226
- requestBody: entityToCreateDto(record),
227
- });
228
- } else {
229
- await onUpdate({
230
- ...pathParams,
231
- ...record,
232
- requestBody: entityToUpdateDto({
233
- ...pathParams,
234
- ...record,
235
- }),
236
- })
237
- }
238
-
239
- if (typeof afterSave === 'function') {
240
- await afterSave(record);
241
- }
242
-
243
- flushSync(() => {
244
- actionRef?.current?.reload();
245
- });
246
- },
247
- async onCancel(
248
- id,
249
- record,
250
- origin,
251
- ) {
252
- if (record) {
253
- Object.assign(record, origin);
254
- }
255
- },
256
- async onDelete(id, row) {
257
- await onDelete({ ...row, ...pathParams });
258
- },
259
- deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
260
- onlyAddOneLineAlertMessage: intl.formatMessage({ id: 'table.onlyAddOneLineAlertMessage' }),
261
- cancelText: <Tooltip title={intl.formatMessage({ id: 'table.cancelText' })}><StopOutlined /></Tooltip>,
262
- deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
263
- saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
264
- ...editable,
265
- }}
186
+ editable={editableConfig}
266
187
  toolBarRender={(...args) => [
267
- ...toolBarRender && toolBarRender(...args) || [],
268
188
  columnsSetSelect?.() || null,
269
189
  !viewOnly && onUpdateMany
270
- ? (
271
- <BulkEditButton
272
- selectedRecords={selectedRecords}
273
- lastRequest={lastRequest}
274
- allSelected={allSelected}
275
- columns={columns}
276
- idColumnName={idColumnName}
277
- // @ts-ignore
278
- onSubmit={values => onUpdateMany({
279
- ...pathParams,
280
- ...lastRequest[0],
281
- requestBody: {
282
- updateValues: _.pickBy(
283
- // @ts-ignore
284
- entityToUpdateDto({
285
- ...pathParams,
286
- ...values,
287
- }),
288
- (value, key) => _.has(values, key),
289
- ),
290
- records: allSelected ? [] : selectedRecords,
291
- },
292
- }).then(() => {
293
- messageApi.open({
294
- type: 'success',
295
- content: 'Operation Successful',
296
- });
297
- actionRef?.current?.reload();
298
- })}
299
- />
300
- )
301
- : <></>,
190
+ ? bulkEditButton
191
+ : null,
302
192
  !viewOnly && onDeleteMany
303
- ? (
304
- <BulkDeleteButton
305
- selectedRecords={selectedRecords}
306
- lastRequest={lastRequest}
307
- allSelected={allSelected}
308
- // @ts-ignore
309
- onDelete={() => onDeleteMany({
310
- ...pathParams,
311
- ...lastRequest[0],
312
- requestBody: {
313
- records: allSelected ? [] : selectedRecords,
314
- },
315
- }).then(() => {
316
- messageApi.open({
317
- type: 'success',
318
- content: 'Operation Successful',
319
- });
320
- actionRef?.current?.reload();
321
- })}
322
- />
323
- )
324
- : <></>,
193
+ ? bulkDeleteButton
194
+ : null,
325
195
  !viewOnly && createButton || null,
196
+ ...toolBarRender && toolBarRender(...args) || [],
326
197
  ]}
327
198
  columns={columns}
328
199
  defaultSize='small'
@@ -331,71 +202,37 @@ const Table = <Entity extends Record<string | symbol, any>,
331
202
  {
332
203
  ...(
333
204
  !viewOnly && (onUpdateMany || onDeleteMany)
334
- ? {
335
- rowSelection: {
336
- selectedRowKeys: selectedRecords.map(record => Array.isArray(idColumnName) ? idColumnName.map(colName => record[colName]).join('-') : record[idColumnName]),
337
- selections: [
338
- {
339
- key: 'all',
340
- text: (
341
- <Space>
342
- Select ALL
343
- <Popover
344
- content={(
345
- <div style={{ width: '100%' }}>
346
- This includes records from ALL pages of the table.
347
- </div>
348
- )}
349
- title={'Select All'}
350
- trigger={['hover', 'click']}
351
- zIndex={1080}
352
- >
353
- <QuestionCircleTwoTone />
354
- </Popover>
355
- </Space>
356
- ),
357
- onSelect: () => {
358
- setSelectedRecords(lastRequest[1].data);
359
- setAllSelected(true);
360
- },
361
- },
362
- ],
363
- onChange: (rowKeys, records) => {
364
- setSelectedRecords(records);
365
- allSelected && setAllSelected(false);
366
- },
367
- }
368
- }
205
+ ? { rowSelection }
369
206
  : {}
370
207
  )
371
208
  }
372
209
  {...rest}
373
210
  />
374
- <DescriptionsCreateModal<Entity>
375
- data={createPopupData}
376
- onClose={() => setCreatePopupData(undefined)}
377
- onSubmit={async (data) => {
378
- try {
379
- await onCreate?.({
380
- ...pathParams,
381
- requestBody: entityToCreateDto({
382
- ...pathParams,
383
- ...data,
384
- })
385
- });
386
- actionRef?.current?.reload();
387
- setCreatePopupData(undefined);
388
- }
389
- catch (e) {
390
- console.error(e);
391
- }
211
+
212
+ {creationModal}
213
+
214
+ <Modal
215
+ title={editPopupTitle}
216
+ open={updatePopupData !== undefined}
217
+ width='80%'
218
+ closeIcon={true}
219
+ footer={null}
220
+ onCancel={() => {
221
+ actionRef?.current?.reload();
222
+ setUpdatePopupData(undefined);
392
223
  }}
393
- idColumnName={idColumnName}
394
- columns={columns ?? []}
395
- />
396
- {contextHolder}
224
+ >
225
+ <Descriptions<Entity, CreateDto, UpdateDto, TPathParams>
226
+ mainTitle={descriptionsMainTitle}
227
+ columns={columns ?? []}
228
+ entity={updatePopupData}
229
+ canEdit={true}
230
+ onUpdate={onUpdate}
231
+ entityToUpdateDto={entityToUpdateDto}
232
+ />
233
+ </Modal>
234
+ {messagesContext}
397
235
  </>);
398
236
  };
399
237
 
400
238
  export default Table;
401
-
@@ -0,0 +1,79 @@
1
+ import { TFilterParams, TFilters, TGetAllParams, TSearchableColumn, TSort } from "./tableTypes";
2
+ import { applyKeywordToSearch, buildJoinFields, collectFieldsFromColumns, getFiltersSearch } from "./tableTools";
3
+ import { QuerySortArr } from "@nestjsx/crud-request";
4
+ import { ProColumns } from "@ant-design/pro-components";
5
+ import { ColumnStateType } from "@ant-design/pro-table/es/typing";
6
+
7
+ export function getTableDataQueryParams<Entity, TPathParams extends Record<string, string | number> = {}>({
8
+ params,
9
+ sort = {},
10
+ filters = {},
11
+ pathParams,
12
+ defaultSort,
13
+ searchableColumns,
14
+ columns = [],
15
+ idColumnName = 'id',
16
+ columnsState,
17
+ }: {
18
+ params: TFilterParams,
19
+ sort?: TSort,
20
+ filters?: TFilters,
21
+ pathParams: TPathParams,
22
+ defaultSort?: QuerySortArr,
23
+ searchableColumns?: TSearchableColumn[],
24
+ columns?: ProColumns<Entity>[],
25
+ idColumnName?: string | string[];
26
+ columnsState?: ColumnStateType;
27
+ }): TGetAllParams & TPathParams {
28
+ const {
29
+ current,
30
+ pageSize,
31
+ keyword,
32
+ baseFilters,
33
+ join,
34
+ sortMap,
35
+ ...filtersFromSearchForm
36
+ } = params;
37
+
38
+ const queryParams: TGetAllParams & TPathParams = {
39
+ ...pathParams,
40
+ page: current,
41
+ limit: pageSize,
42
+ };
43
+
44
+ const sortBy = Object
45
+ .entries(sort)
46
+ .reduce<string[]>(
47
+ (data: string[], [key, direction]) => {
48
+ data.push(`${sortMap?.[key] || key},${direction === 'ascend' ? 'ASC' : 'DESC'}`);
49
+ return data;
50
+ },
51
+ []
52
+ );
53
+ if (!sortBy.length && defaultSort) {
54
+ sortBy.push(defaultSort.join(','));
55
+ }
56
+ queryParams.sort = sortBy;
57
+
58
+ let search = getFiltersSearch({
59
+ baseFilters,
60
+ filters: {
61
+ ...filters,
62
+ ...filtersFromSearchForm,
63
+ },
64
+ searchableColumns,
65
+ });
66
+ search = applyKeywordToSearch(search, searchableColumns!, columnsState.value!, keyword);
67
+ queryParams.s = JSON.stringify(search);
68
+
69
+ const { joinSelect, joinFields } = buildJoinFields(join);
70
+ queryParams.join = joinSelect;
71
+
72
+ queryParams.fields = columns && collectFieldsFromColumns(
73
+ columns,
74
+ idColumnName,
75
+ joinFields,
76
+ ) || [];
77
+
78
+ return queryParams;
79
+ }
@@ -4,3 +4,4 @@ export * from './tableTools';
4
4
  export * from './tableTypes';
5
5
  export * from './useColumnsSets';
6
6
  export { default as useColumnsSets } from './useColumnsSets';
7
+ export * from "./useCreation";
@@ -169,11 +169,12 @@ export function buildFieldsFromColumns<T>(
169
169
  // skip id column because it is always included by backend
170
170
  // and join fields because they are included by join
171
171
 
172
- if (!col.dataIndex || (Array.isArray(idColumnName) ? idColumnName.includes(col.dataIndex) : col.dataIndex === idColumnName) || joinFields.has(col.dataIndex as string)) {
172
+ const dataIndex = String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex);
173
+ if (!dataIndex || (Array.isArray(idColumnName) ? idColumnName.includes(dataIndex) : dataIndex === idColumnName) || joinFields.has(dataIndex)) {
173
174
  return;
174
175
  }
175
176
 
176
- fields.add(String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex));
177
+ fields.add(dataIndex);
177
178
  });
178
179
 
179
180
  return fields;
@@ -5,6 +5,7 @@ import { Operators } from "./tableTools";
5
5
  import { TColumnsSet } from "./useColumnsSets";
6
6
  import { ColumnStateType } from "@ant-design/pro-table/es/typing";
7
7
  import { RowEditableConfig } from "@ant-design/pro-utils";
8
+ import { ProColumns } from "@ant-design/pro-components";
8
9
 
9
10
  export type IWithId = {
10
11
  id: string | number,
@@ -88,9 +89,7 @@ interface BaseProps<Entity,
88
89
  pathParams: TPathParams;
89
90
  idColumnName?: string & keyof Entity | (string & keyof Entity)[];
90
91
  createNewDefaultParams?: Partial<Entity>;
91
- afterSave?: (record: Entity) => Promise<void>;
92
- actionRef?: MutableRefObject<ActionType | undefined>;
93
- editable?: RowEditableConfig<Entity>;
92
+ editableRecord?: Partial<Entity>;
94
93
  defaultSort?: QuerySortArr;
95
94
  searchableColumns?: TSearchableColumn[];
96
95
  viewOnly?: boolean;
@@ -98,17 +97,25 @@ interface BaseProps<Entity,
98
97
  popupCreation?: boolean;
99
98
  columnsState?: ColumnStateType;
100
99
  columnsSetSelect?: () => React.ReactNode;
101
- popupDataState?: [Partial<Entity>, React.Dispatch<React.SetStateAction<Partial<Entity>>>]
100
+ editPopupTitle?: string;
101
+ createPopupTitle?: string;
102
+ descriptionsMainTitle?: ProColumns<Entity>['title'] | null;
102
103
  }
103
104
 
104
- interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
105
+ export interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
106
+ actionRef?: MutableRefObject<ActionType | undefined>;
107
+ editable?: RowEditableConfig<Entity>;
108
+ afterSave?: (record: Entity) => Promise<void>;
105
109
  onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
106
- onUpdate: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>;
107
- onDelete: ({}: Record<keyof Entity, string> & TPathParams) => Promise<void>;
110
+ onUpdate: ({}: Partial<Entity> & {
111
+ requestBody: UpdateDto,
112
+ index?: number,
113
+ } & TPathParams) => Promise<Entity>;
114
+ onDelete: ({}: Partial<Entity> & TPathParams) => Promise<void>;
108
115
  entityToCreateDto: (entity: Entity) => CreateDto;
109
116
  entityToUpdateDto: (entity: Entity) => UpdateDto;
110
- onUpdateMany: ({}: Record<keyof Entity, string> & { requestBody: { updateValues: Partial<UpdateDto>[], records: Entity[] } } & TPathParams) => Promise<void>,
111
- onDeleteMany: ({}: Record<keyof Entity, string> & { requestBody: { records: Entity[] } } & TPathParams) => Promise<void>,
117
+ onUpdateMany: ({}: Partial<Entity> & { requestBody: { updateValues: Partial<UpdateDto>[], records: Entity[] } } & TPathParams) => Promise<void>,
118
+ onDeleteMany: ({}: Partial<Entity> & { requestBody: { records: Entity[] } } & TPathParams) => Promise<void>,
112
119
  }
113
120
 
114
121
  // Conditional type to merge base and editable props conditionally