@boarteam/boar-pack-common-frontend 2.0.1

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.
Files changed (36) hide show
  1. package/package.json +50 -0
  2. package/src/components/Descriptions/Descriptions.tsx +166 -0
  3. package/src/components/Descriptions/DescriptionsCreateModal.tsx +65 -0
  4. package/src/components/Descriptions/descriptionTypes.ts +49 -0
  5. package/src/components/Descriptions/index.ts +5 -0
  6. package/src/components/Descriptions/useDescriptionColumns.ts +37 -0
  7. package/src/components/Inputs/DateRange.tsx +75 -0
  8. package/src/components/Inputs/MultiStringSelect.tsx +20 -0
  9. package/src/components/Inputs/NumberInputHandlingNewRecord.tsx +11 -0
  10. package/src/components/Inputs/NumberSwitcher.tsx +27 -0
  11. package/src/components/Inputs/Password.tsx +33 -0
  12. package/src/components/Inputs/RelationSelect.tsx +72 -0
  13. package/src/components/Inputs/SearchSelect.tsx +93 -0
  14. package/src/components/Inputs/filterDropdowns.tsx +103 -0
  15. package/src/components/Inputs/index.ts +9 -0
  16. package/src/components/Inputs/useCheckConnection.tsx +79 -0
  17. package/src/components/List/List.tsx +266 -0
  18. package/src/components/List/index.ts +3 -0
  19. package/src/components/List/listTypes.ts +31 -0
  20. package/src/components/QuestionMarkHint/QuestionMarkHint.tsx +33 -0
  21. package/src/components/QuestionMarkHint/index.ts +1 -0
  22. package/src/components/Table/BulkDeleteButton.tsx +55 -0
  23. package/src/components/Table/BulkEditButton.tsx +160 -0
  24. package/src/components/Table/Table.tsx +400 -0
  25. package/src/components/Table/index.ts +6 -0
  26. package/src/components/Table/tableTools.ts +168 -0
  27. package/src/components/Table/tableTypes.ts +127 -0
  28. package/src/components/Table/useColumnsSets.tsx +110 -0
  29. package/src/components/index.ts +5 -0
  30. package/src/index.ts +2 -0
  31. package/src/tools/WebsocketClient.ts +138 -0
  32. package/src/tools/index.ts +5 -0
  33. package/src/tools/numberTools.ts +6 -0
  34. package/src/tools/safetyRun.ts +5 -0
  35. package/src/tools/useFullscreen.tsx +62 -0
  36. package/src/tools/useTabs.ts +17 -0
@@ -0,0 +1,160 @@
1
+ import { ProColumns } from "@ant-design/pro-table";
2
+ import React, { useEffect, useState } from "react";
3
+ import { Button, Checkbox, Modal, Popconfirm } from "antd";
4
+ import { LoadingOutlined } from "@ant-design/icons";
5
+ import { columnsToDescriptionItemProps } from "../Descriptions";
6
+ import { useForm } from "antd/lib/form/Form";
7
+ import { ProDescriptions } from "@ant-design/pro-components";
8
+ import { TGetAllParams } from "./tableTypes";
9
+ import { createStyles } from "antd-style";
10
+
11
+ type TBulkEditConfig<Entity> = { type: 'records', value: Entity[], count: number } | { type: 'query', value: Record<string, any>, count: number } | null;
12
+
13
+ const useStyles = createStyles(() => {
14
+ return {
15
+ popconfirm: {
16
+ '.ant-popconfirm-description': {
17
+ marginTop: '0 !important',
18
+ },
19
+ }
20
+ }
21
+ })
22
+
23
+ const BulkEditDialog = <Entity extends Record<string | symbol, any>>(
24
+ {
25
+ columns,
26
+ idColumnName,
27
+ config,
28
+ onClose,
29
+ onSubmit,
30
+ }: {
31
+ idColumnName: string & keyof Entity | (string & keyof Entity)[],
32
+ columns: ProColumns<Entity>[],
33
+ config: TBulkEditConfig<Entity>,
34
+ onClose: () => void,
35
+ onSubmit: (value: Partial<Entity>) => Promise<void>
36
+ }) => {
37
+ const [loading, setLoading] = useState(false);
38
+ const sections = columnsToDescriptionItemProps(columns, 'General');
39
+ const { styles } = useStyles();
40
+
41
+ const [editableKeys, setEditableKeys] = useState<Set<string>>(new Set);
42
+
43
+ const [form] = useForm();
44
+ useEffect(() => {
45
+ setEditableKeys(new Set);
46
+ form.resetFields();
47
+ }, [config]);
48
+
49
+ const handleCheckboxChange = (dataIndex: string, checked: boolean) => {
50
+ setEditableKeys(prev => {
51
+ const next = new Set(prev);
52
+ checked ? next.add(dataIndex) : next.delete(dataIndex);
53
+ return next;
54
+ });
55
+ };
56
+
57
+ const handleUpdate = async () => {
58
+ await form.validateFields();
59
+ setLoading(true);
60
+ await onSubmit(form.getFieldsValue()).finally(() => setLoading(false));
61
+ onClose();
62
+ };
63
+
64
+ return (
65
+ <Modal
66
+ title={`Updating ${config?.count} ${config?.count === 1 ? 'record' : 'records'}...`}
67
+ open={config !== null}
68
+ onCancel={onClose}
69
+ width='80%'
70
+ footer={[
71
+ <Popconfirm
72
+ overlayClassName={styles.popconfirm}
73
+ title={false}
74
+ description={`Are you sure you want to update ${config?.count} ${config?.count === 1 ? 'record' : 'records'}?`}
75
+ onConfirm={() => handleUpdate()}
76
+ okText="Yes"
77
+ cancelText="No"
78
+ >
79
+ <Button key='submit' type="primary">Update {loading && <LoadingOutlined />}</Button>
80
+ </Popconfirm>
81
+ ]}
82
+ >
83
+ {sections.map((section) => {
84
+ return (
85
+ <ProDescriptions<Entity>
86
+ extra={'Click on the field first and then type a new desired value.'}
87
+ key={Array.isArray(idColumnName) ? idColumnName.join('-') : idColumnName}
88
+ title={section.title as React.ReactNode}
89
+ size={"small"}
90
+ bordered
91
+ column={3}
92
+ style={{ marginBottom: 20 }}
93
+ labelStyle={{ width: '15%' }}
94
+ contentStyle={{ width: '25%' }}
95
+ editable={{
96
+ form,
97
+ editableKeys: [...editableKeys],
98
+ actionRender: () => [],
99
+ }}
100
+ columns={section.columns.filter(column => column?.editable === undefined).map(column => ({
101
+ ...column,
102
+ render: (...params) => !editableKeys.has(column.dataIndex) ? '(This field will not be changed)' : column.render(...params),
103
+ editable: false,
104
+ title: (
105
+ <Checkbox
106
+ checked={editableKeys.has(column.dataIndex)}
107
+ onChange={e => handleCheckboxChange(column.dataIndex, e.target.checked)}
108
+ >
109
+ {column.title}
110
+ </Checkbox>
111
+ ),
112
+ }))}
113
+ />
114
+ )
115
+ })}
116
+ </Modal>
117
+ );
118
+ }
119
+
120
+ const BulkEditButton = <Entity extends Record<string | symbol, any>>(
121
+ {
122
+ selectedRecords,
123
+ lastRequest,
124
+ allSelected,
125
+ columns,
126
+ idColumnName,
127
+ onSubmit,
128
+ } : {
129
+ selectedRecords: Entity[],
130
+ lastRequest: [TGetAllParams & Record<string, string | number>, any] | [],
131
+ idColumnName: string & keyof Entity | (string & keyof Entity)[],
132
+ allSelected: boolean,
133
+ columns: ProColumns<Entity>[],
134
+ onSubmit: (value: Partial<Entity>) => Promise<void>
135
+ }) => {
136
+ const [bulkEditConfig, setBulkEditConfig] = useState<TBulkEditConfig<Entity>>(null);
137
+ const recordsCount = allSelected ? lastRequest[1].total : selectedRecords.length;
138
+
139
+ return (<>
140
+ <Button
141
+ disabled={recordsCount === 0}
142
+ onClick={() => setBulkEditConfig(
143
+ !allSelected
144
+ ? { type: 'records', value: selectedRecords, count: selectedRecords.length }
145
+ : { type: 'query', value: lastRequest[0], count: lastRequest[1].total }
146
+ )}
147
+ >
148
+ {recordsCount > 0 ? `Edit ${recordsCount} ${recordsCount === 1 ? 'Record' : 'Records'}` : 'Bulk Edit'}
149
+ </Button>
150
+ <BulkEditDialog<Entity>
151
+ config={bulkEditConfig}
152
+ onClose={() => setBulkEditConfig(null)}
153
+ columns={columns}
154
+ idColumnName={idColumnName}
155
+ onSubmit={onSubmit}
156
+ />
157
+ </>);
158
+ };
159
+
160
+ export default BulkEditButton;
@@ -0,0 +1,400 @@
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";
9
+ 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
+ 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
+ }
28
+
29
+ const useStyles = createStyles(() => {
30
+ return {
31
+ table: {
32
+ '.ant-pro-table-alert': {
33
+ display: 'none',
34
+ },
35
+ }
36
+ }
37
+ })
38
+
39
+ const Table = <Entity extends Record<string | symbol, any>,
40
+ CreateDto = Entity,
41
+ UpdateDto = Entity,
42
+ TEntityParams = {},
43
+ TPathParams extends Record<string, string | number> = {},
44
+ TKey = string,
45
+ >(
46
+ {
47
+ getAll,
48
+ onCreate,
49
+ onUpdate,
50
+ onUpdateMany,
51
+ onDelete,
52
+ onDeleteMany,
53
+ pathParams,
54
+ idColumnName = 'id',
55
+ entityToCreateDto,
56
+ entityToUpdateDto,
57
+ createNewDefaultParams,
58
+ afterSave,
59
+ actionRef: actionRefProp,
60
+ editable,
61
+ defaultSort = ['createdAt', 'DESC'],
62
+ searchableColumns = [],
63
+ viewOnly = false,
64
+ columns = [],
65
+ columnsSets,
66
+ columnsState: managedColumnsState,
67
+ columnsSetSelect: managedColumnsSetSelect,
68
+ popupCreation = false,
69
+ toolBarRender,
70
+ params,
71
+ ...rest
72
+ }: TTableProps<Entity,
73
+ CreateDto,
74
+ UpdateDto,
75
+ TEntityParams,
76
+ TPathParams>
77
+ ) => {
78
+ const actionRefComponent = useRef<ActionType>();
79
+ const actionRef = actionRefProp || actionRefComponent;
80
+ const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
81
+ const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
82
+ const [selectedRecords, setSelectedRecords] = useState<Entity[]>([]);
83
+ const [lastRequest, setLastRequest] = useState<[TGetAllParams & TPathParams, any] | []>([]);
84
+ const [allSelected, setAllSelected] = useState(false);
85
+ const { styles } = useStyles();
86
+ const [messageApi, contextHolder] = message.useMessage();
87
+
88
+ const intl = useIntl();
89
+
90
+ useEffect(() => {
91
+ actionRef?.current?.reload();
92
+ }, [JSON.stringify(pathParams), JSON.stringify(params)]);
93
+
94
+ const {
95
+ columnsSetSelect: localColumnsSetSelect,
96
+ columnsState: localColumnsState,
97
+ } = useColumnsSets<Entity>({
98
+ columns,
99
+ columnsSets,
100
+ });
101
+
102
+ const columnsState = managedColumnsState ?? localColumnsState;
103
+ const columnsSetSelect = managedColumnsSetSelect ?? localColumnsSetSelect;
104
+
105
+ const request = async (
106
+ params: TFilterParams,
107
+ sort: TSort = {},
108
+ filters: TFilters = {},
109
+ ) => {
110
+ const {
111
+ current,
112
+ pageSize,
113
+ keyword,
114
+ baseFilters,
115
+ join,
116
+ sortMap,
117
+ ...filtersFromSearchForm
118
+ } = params;
119
+
120
+ const queryParams: TGetAllParams & TPathParams = {
121
+ ...pathParams,
122
+ page: current,
123
+ limit: pageSize,
124
+ };
125
+
126
+ const sortBy = Object
127
+ .entries(sort)
128
+ .reduce<string[]>(
129
+ (data: string[], [key, direction]) => {
130
+ data.push(`${sortMap?.[key] || key},${direction === 'ascend' ? 'ASC' : 'DESC'}`);
131
+ return data;
132
+ },
133
+ []
134
+ );
135
+ if (!sortBy.length && defaultSort) {
136
+ sortBy.push(defaultSort.join(','));
137
+ }
138
+ queryParams.sort = sortBy;
139
+
140
+ let search = getFiltersSearch({
141
+ baseFilters,
142
+ filters: {
143
+ ...filters,
144
+ ...filtersFromSearchForm,
145
+ },
146
+ searchableColumns,
147
+ });
148
+ search = applyKeywordToSearch(search, searchableColumns!, columnsState.value!, keyword);
149
+ queryParams.s = JSON.stringify(search);
150
+
151
+ const { joinSelect, joinFields } = buildJoinFields(join);
152
+ queryParams.join = joinSelect;
153
+
154
+ queryParams.fields = columns && collectFieldsFromColumns(
155
+ columns,
156
+ idColumnName,
157
+ joinFields,
158
+ ) || [];
159
+
160
+ const result = await getAll(queryParams);
161
+
162
+ setSelectedRecords([]);
163
+ setLastRequest([
164
+ queryParams,
165
+ result,
166
+ ]);
167
+ return result;
168
+ }
169
+
170
+ const createButton = <Button
171
+ size={rest.size}
172
+ type="primary"
173
+ key="create"
174
+ onClick={() => {
175
+ if (popupCreation) {
176
+ setCreatePopupData(createNewDefaultParams);
177
+ } else {
178
+ actionRef?.current?.addEditRecord({
179
+ [KEY_SYMBOL]: getNewId(),
180
+ ...createNewDefaultParams,
181
+ }, {
182
+ position: 'top',
183
+ });
184
+ }
185
+ }}
186
+ >
187
+ <PlusOutlined /> <FormattedMessage id={'table.newButton'} />
188
+ </Button>;
189
+
190
+ return (<>
191
+ <ProTable<Entity, TEntityParams & TFilterParams>
192
+ actionRef={actionRef}
193
+ className={styles.table}
194
+ request={request}
195
+ rowKey={record => record[KEY_SYMBOL] ?? (Array.isArray(idColumnName) ? idColumnName.map(colName => record[colName]).join('-') : record[idColumnName])}
196
+ options={{
197
+ fullScreen: true,
198
+ reload: true,
199
+ search: {
200
+ allowClear: true,
201
+ },
202
+ density: true,
203
+ setting: {
204
+ draggable: false,
205
+ checkable: true,
206
+ checkedReset: true,
207
+ listsHeight: 500,
208
+ },
209
+ }}
210
+ bordered
211
+ search={false}
212
+ editable={{
213
+ type: 'multiple',
214
+ editableKeys,
215
+ onChange: setEditableRowKeys,
216
+ async onSave(
217
+ id,
218
+ record,
219
+ origin,
220
+ newLine,
221
+ ) {
222
+ if (newLine) {
223
+ await onCreate?.({
224
+ ...pathParams,
225
+ requestBody: entityToCreateDto(record),
226
+ });
227
+ } else {
228
+ await onUpdate({
229
+ ...pathParams,
230
+ ...record,
231
+ requestBody: entityToUpdateDto({
232
+ ...pathParams,
233
+ ...record,
234
+ }),
235
+ })
236
+ }
237
+
238
+ if (typeof afterSave === 'function') {
239
+ await afterSave(record);
240
+ }
241
+
242
+ flushSync(() => {
243
+ actionRef?.current?.reload();
244
+ });
245
+ },
246
+ async onCancel(
247
+ id,
248
+ record,
249
+ origin,
250
+ ) {
251
+ if (record) {
252
+ Object.assign(record, origin);
253
+ }
254
+ },
255
+ async onDelete(id, row) {
256
+ await onDelete({ ...row, ...pathParams });
257
+ },
258
+ deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
259
+ onlyAddOneLineAlertMessage: intl.formatMessage({ id: 'table.onlyAddOneLineAlertMessage' }),
260
+ cancelText: <Tooltip title={intl.formatMessage({ id: 'table.cancelText' })}><StopOutlined /></Tooltip>,
261
+ deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
262
+ saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
263
+ ...editable,
264
+ }}
265
+ toolBarRender={(...args) => [
266
+ ...toolBarRender && toolBarRender(...args) || [],
267
+ columnsSetSelect?.() || null,
268
+ !viewOnly && onUpdateMany
269
+ ? (
270
+ <BulkEditButton
271
+ selectedRecords={selectedRecords}
272
+ lastRequest={lastRequest}
273
+ allSelected={allSelected}
274
+ columns={columns}
275
+ idColumnName={idColumnName}
276
+ // @ts-ignore
277
+ onSubmit={values => onUpdateMany({
278
+ ...pathParams,
279
+ ...lastRequest[0],
280
+ requestBody: {
281
+ updateValues: _.pickBy(
282
+ // @ts-ignore
283
+ entityToUpdateDto({
284
+ ...pathParams,
285
+ ...values,
286
+ }),
287
+ (value, key) => _.has(values, key),
288
+ ),
289
+ records: allSelected ? [] : selectedRecords,
290
+ },
291
+ }).then(() => {
292
+ messageApi.open({
293
+ type: 'success',
294
+ content: 'Operation Successful',
295
+ });
296
+ actionRef?.current?.reload();
297
+ })}
298
+ />
299
+ )
300
+ : <></>,
301
+ !viewOnly && onDeleteMany
302
+ ? (
303
+ <BulkDeleteButton
304
+ selectedRecords={selectedRecords}
305
+ lastRequest={lastRequest}
306
+ allSelected={allSelected}
307
+ // @ts-ignore
308
+ onDelete={() => onDeleteMany({
309
+ ...pathParams,
310
+ ...lastRequest[0],
311
+ requestBody: {
312
+ records: allSelected ? [] : selectedRecords,
313
+ },
314
+ }).then(() => {
315
+ messageApi.open({
316
+ type: 'success',
317
+ content: 'Operation Successful',
318
+ });
319
+ actionRef?.current?.reload();
320
+ })}
321
+ />
322
+ )
323
+ : <></>,
324
+ !viewOnly && createButton || null,
325
+ ]}
326
+ columns={columns}
327
+ defaultSize='small'
328
+ columnsState={columnsState}
329
+ params={params}
330
+ {
331
+ ...(
332
+ !viewOnly && (onUpdateMany || onDeleteMany)
333
+ ? {
334
+ rowSelection: {
335
+ selectedRowKeys: selectedRecords.map(record => Array.isArray(idColumnName) ? idColumnName.map(colName => record[colName]).join('-') : record[idColumnName]),
336
+ selections: [
337
+ {
338
+ key: 'all',
339
+ text: (
340
+ <Space>
341
+ Select ALL
342
+ <Popover
343
+ content={(
344
+ <div style={{ width: '100%' }}>
345
+ This includes records from ALL pages of the table.
346
+ </div>
347
+ )}
348
+ title={'Select All'}
349
+ trigger={['hover', 'click']}
350
+ zIndex={1080}
351
+ >
352
+ <QuestionCircleTwoTone />
353
+ </Popover>
354
+ </Space>
355
+ ),
356
+ onSelect: () => {
357
+ setSelectedRecords(lastRequest[1].data);
358
+ setAllSelected(true);
359
+ },
360
+ },
361
+ ],
362
+ onChange: (rowKeys, records) => {
363
+ setSelectedRecords(records);
364
+ allSelected && setAllSelected(false);
365
+ },
366
+ }
367
+ }
368
+ : {}
369
+ )
370
+ }
371
+ {...rest}
372
+ />
373
+ <DescriptionsCreateModal<Entity>
374
+ data={createPopupData}
375
+ onClose={() => setCreatePopupData(undefined)}
376
+ onSubmit={async (data) => {
377
+ try {
378
+ await onCreate?.({
379
+ ...pathParams,
380
+ requestBody: entityToCreateDto({
381
+ ...pathParams,
382
+ ...data,
383
+ })
384
+ });
385
+ actionRef?.current?.reload();
386
+ setCreatePopupData(undefined);
387
+ }
388
+ catch (e) {
389
+ console.error(e);
390
+ }
391
+ }}
392
+ idColumnName={idColumnName}
393
+ columns={columns ?? []}
394
+ />
395
+ {contextHolder}
396
+ </>);
397
+ };
398
+
399
+ export default Table;
400
+
@@ -0,0 +1,6 @@
1
+ export * from './Table';
2
+ export { default as Table } from './Table'
3
+ export * from './tableTools';
4
+ export * from './tableTypes';
5
+ export * from './useColumnsSets';
6
+ export { default as useColumnsSets } from './useColumnsSets';