@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,93 @@
1
+ import { useState, useEffect } from "react";
2
+ import { SearchOutlined } from "@ant-design/icons";
3
+ import { Checkbox, Empty, Input, Menu } from "antd";
4
+ import { FilterDropdownProps } from "antd/es/table/interface";
5
+
6
+ export const SearchSelect = function<T>({
7
+ selectedKeys,
8
+ setSelectedKeys,
9
+ filter = [],
10
+ limit = 7,
11
+ fetchItems,
12
+ fieldNames = {
13
+ value: 'id',
14
+ label: 'name',
15
+ },
16
+ }: Pick<FilterDropdownProps, 'selectedKeys' | 'setSelectedKeys'> & {
17
+ filter?: string[],
18
+ limit?: number,
19
+ fetchItems: (filter: string[], limit?: number, keyword?: string) => Promise<{ data: T[] }>,
20
+ fieldNames?: {
21
+ value: string,
22
+ label: string,
23
+ },
24
+ }) {
25
+ const { value: valueKey, label: labelKey } = fieldNames;
26
+
27
+ const [availableItems, setAvailableItems] = useState<T[]>([]);
28
+ const request = async (keyword: string) => {
29
+ const reqFilter = [...filter];
30
+ if (keyword) {
31
+ reqFilter.push(labelKey + '||$contL||' + keyword);
32
+ }
33
+
34
+ const resp = await fetchItems(reqFilter, limit, keyword);
35
+ setAvailableItems(resp.data);
36
+ }
37
+
38
+ useEffect(() => {
39
+ request('');
40
+ }, []);
41
+
42
+ return (
43
+ <>
44
+ <div className={`ant-table-filter-dropdown-search`}>
45
+ <Input
46
+ prefix={<SearchOutlined />}
47
+ placeholder={'Search in filters'}
48
+ onChange={e => request(e.target.value)}
49
+ className={`ant-table-filter-dropdown-search-input`}
50
+ />
51
+ </div>
52
+ {
53
+ availableItems.length === 0
54
+ ? (
55
+ <Empty
56
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
57
+ description={'Not Found'}
58
+ imageStyle={{
59
+ height: 24,
60
+ }}
61
+ style={{
62
+ margin: 0,
63
+ padding: '16px 0',
64
+ }}
65
+ />
66
+ )
67
+ : (
68
+ <Menu
69
+ selectable
70
+ multiple
71
+ prefixCls={`ant-dropdown-menu`}
72
+ className={'ant-dropdown-menu'}
73
+ onSelect={({ key }) => setSelectedKeys([...selectedKeys, key])}
74
+ onDeselect={({ key }) => setSelectedKeys(selectedKeys.filter(selectedKey => selectedKey !== key))}
75
+ // @ts-ignore
76
+ selectedKeys={selectedKeys}
77
+ items={availableItems.map(item => (
78
+ {
79
+ key: String(item?.[valueKey as keyof T]),
80
+ label: (
81
+ <>
82
+ <Checkbox checked={selectedKeys?.includes(String(item?.[valueKey as keyof T]))} />
83
+ <span>{String(item?.[labelKey as keyof T])}</span>
84
+ </>
85
+ ),
86
+ }
87
+ ))}
88
+ />
89
+ )
90
+ }
91
+ </>
92
+ );
93
+ }
@@ -0,0 +1,103 @@
1
+ import { Tag, Input, InputNumber, Space, Button } from "antd";
2
+ import { ColumnFilterItem, FilterDropdownProps } from "antd/es/table/interface";
3
+ import { ReactNode, useEffect, useState } from "react";
4
+
5
+ export const booleanFilters: ColumnFilterItem[] = [
6
+ { text: <Tag color='red'>Disabled</Tag>, value: 0 },
7
+ { text: <Tag color='green'>Enabled</Tag>, value: 1 },
8
+ ];
9
+
10
+ export function NumberFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }: FilterDropdownProps) {
11
+ return (
12
+ <DynamicOptionsFilterDropdown confirm={confirm} clearFilters={clearFilters}>
13
+ <InputNumber
14
+ value={selectedKeys.length ? Number(selectedKeys[0]) : undefined}
15
+ onChange={(value) => setSelectedKeys(value === undefined ? [] : [value])}
16
+ onPressEnter={() => confirm()}
17
+ step={1}
18
+ style={{ margin: 4, width: 250 }}
19
+ placeholder="Please Enter"
20
+ />
21
+ </DynamicOptionsFilterDropdown>
22
+ )
23
+ }
24
+
25
+ export function NumberRangeFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }: FilterDropdownProps) {
26
+ const [range, updateRange] = useState<[number, number] | undefined>(selectedKeys);
27
+
28
+ useEffect(() => {
29
+ updateRange(selectedKeys);
30
+ }, [selectedKeys]);
31
+
32
+ useEffect(() => {
33
+ setSelectedKeys(range);
34
+ }, [range]);
35
+
36
+ return (
37
+ <DynamicOptionsFilterDropdown confirm={confirm} clearFilters={clearFilters}>
38
+ <InputNumber
39
+ value={range?.[0]}
40
+ onChange={value => updateRange(prev => ([value, prev[1]]))}
41
+ onPressEnter={() => confirm()}
42
+ step={1}
43
+ style={{ margin: 4, width: 250 }}
44
+ placeholder="From"
45
+ />
46
+ <InputNumber
47
+ value={range?.[1]}
48
+ onChange={value => updateRange(prev => ([prev[0], value]))}
49
+ onPressEnter={() => confirm()}
50
+ step={1}
51
+ style={{ margin: 4, width: 250 }}
52
+ placeholder="To"
53
+ />
54
+ </DynamicOptionsFilterDropdown>
55
+ )
56
+ }
57
+
58
+ export function StringFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }: FilterDropdownProps) {
59
+ return (
60
+ <DynamicOptionsFilterDropdown confirm={confirm} clearFilters={clearFilters}>
61
+ <Input
62
+ value={selectedKeys.length ? String(selectedKeys[0]) : undefined}
63
+ onChange={(event) => setSelectedKeys(event.target.value === undefined ? [] : [event.target.value])}
64
+ onPressEnter={() => confirm()}
65
+ style={{ margin: 4, width: 250 }}
66
+ placeholder="Please Enter"
67
+ />
68
+ </DynamicOptionsFilterDropdown>
69
+ )
70
+ }
71
+
72
+ export const DynamicOptionsFilterDropdown = ({
73
+ children,
74
+ confirm,
75
+ clearFilters,
76
+ }: Partial<FilterDropdownProps> & { children: ReactNode}) => {
77
+ return (
78
+ <div style={{ display: 'flex', flexDirection: 'column' }} onKeyDown={(e) => e.stopPropagation()}>
79
+ {children}
80
+ <Space className="ant-table-filter-dropdown-btns">
81
+ <Button
82
+ type="link"
83
+ onClick={() => {
84
+ clearFilters();
85
+ confirm();
86
+ }}
87
+ size="small"
88
+ >
89
+ Reset
90
+ </Button>
91
+ <Button
92
+ type="primary"
93
+ size="small"
94
+ onClick={() => {
95
+ confirm();
96
+ }}
97
+ >
98
+ OK
99
+ </Button>
100
+ </Space>
101
+ </div>
102
+ )
103
+ };
@@ -0,0 +1,9 @@
1
+ export * from './MultiStringSelect';
2
+ export * from './NumberInputHandlingNewRecord';
3
+ export * from './NumberSwitcher';
4
+ export * from './Password';
5
+ export * from './RelationSelect';
6
+ export * from './useCheckConnection';
7
+ export * from './filterDropdowns';
8
+ export * from './SearchSelect';
9
+ export * from './DateRange';
@@ -0,0 +1,79 @@
1
+ import React, { useState } from "react";
2
+ import { Button, message } from "antd";
3
+ import { CheckOutlined, WarningOutlined } from "@ant-design/icons";
4
+ import { green, red } from "@ant-design/colors";
5
+
6
+ type TCheckConnection = {
7
+ defaultSuccessMessage: string;
8
+ defaultErrorMessage: string;
9
+ request: () => Promise<{
10
+ success: boolean;
11
+ message?: string;
12
+ }>;
13
+ }
14
+
15
+ export const useCheckConnection = ({
16
+ defaultSuccessMessage,
17
+ defaultErrorMessage,
18
+ request,
19
+ }: TCheckConnection): {
20
+ button: React.ReactElement;
21
+ } => {
22
+ const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
23
+ const [loading, setLoading] = useState<boolean>(false);
24
+ const [error, setError] = useState<string | null | undefined>(undefined);
25
+ const [messageApi, contextHolder] = message.useMessage();
26
+
27
+ const checkConnection = () => {
28
+ if (timer) {
29
+ clearTimeout(timer);
30
+ setTimer(null);
31
+ }
32
+
33
+ setLoading(true);
34
+ setError(null);
35
+
36
+ request().then(({
37
+ success,
38
+ message,
39
+ }) => {
40
+ if (success) {
41
+ setError(null);
42
+ messageApi.success(message || defaultSuccessMessage);
43
+ } else {
44
+ const err = message || defaultErrorMessage;
45
+ setError(err);
46
+ messageApi.error(err);
47
+ }
48
+ }).catch(e => {
49
+ console.error(e);
50
+ }).finally(() => {
51
+ setLoading(false);
52
+ setTimer(setTimeout(() => {
53
+ setError(undefined);
54
+ }, 3000));
55
+ });
56
+ };
57
+
58
+ let icon = null;
59
+ if (error === null) {
60
+ icon = <CheckOutlined style={{ color: green.primary }} />;
61
+ } else if (error !== undefined) {
62
+ icon = <WarningOutlined style={{ color: red.primary }} />;
63
+ }
64
+
65
+ const button = <>
66
+ {contextHolder}
67
+ <Button
68
+ size={'small'}
69
+ loading={loading}
70
+ danger={!!error}
71
+ icon={icon}
72
+ onClick={checkConnection}
73
+ >Test</Button>
74
+ </>;
75
+
76
+ return {
77
+ button,
78
+ };
79
+ }
@@ -0,0 +1,266 @@
1
+ import { ActionType } from "@ant-design/pro-table";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { Button, Tooltip } from "antd";
4
+ import { DeleteOutlined, PlusOutlined, StopOutlined } from "@ant-design/icons";
5
+ import { FormattedMessage, useIntl } from "react-intl";
6
+ import { flushSync } from "react-dom";
7
+ import { ProList } from "@ant-design/pro-components";
8
+ import { getNewId, KEY_SYMBOL } from "../Table";
9
+ import { createStyles } from "antd-style";
10
+ import { TListProps } from "./listTypes";
11
+ import {
12
+ applyKeywordToSearch,
13
+ buildJoinFields, collectFieldsFromColumns,
14
+ getFiltersSearch,
15
+ TFilterParams,
16
+ TFilters,
17
+ TGetAllParams,
18
+ TSort
19
+ } from "../Table";
20
+ // import DescriptionsCreateModal from "../Descriptions/DescriptionsCreateModal";
21
+
22
+ const useStyles = createStyles(() => {
23
+ return {
24
+ list: {
25
+ '.ant-pro-list-row-editable .ant-form-item': {
26
+ width: '100%',
27
+ },
28
+ '.ant-pro-checkcard-body': {
29
+ padding: '12px !important',
30
+ }
31
+ }
32
+ }
33
+ })
34
+
35
+ const List = <Entity extends Record<string | symbol, any>,
36
+ CreateDto = Entity,
37
+ UpdateDto = Entity,
38
+ TEntityParams = {},
39
+ TPathParams extends Record<string, string | number> = {},
40
+ TKey = string,
41
+ >(
42
+ {
43
+ getAll,
44
+ onCreate,
45
+ onUpdate,
46
+ onDelete,
47
+ pathParams,
48
+ idColumnName = 'id',
49
+ entityToCreateDto,
50
+ entityToUpdateDto,
51
+ createNewDefaultParams,
52
+ afterSave,
53
+ actionRef: actionRefProp,
54
+ editable,
55
+ defaultSort = ['createdAt', 'DESC'],
56
+ searchableColumns = [],
57
+ viewOnly = false,
58
+ columns = [],
59
+ // columnsSets,
60
+ popupCreation = false,
61
+ toolBarRender,
62
+ metas,
63
+ ...rest
64
+ }: TListProps<Entity,
65
+ CreateDto,
66
+ UpdateDto,
67
+ TEntityParams,
68
+ TPathParams>
69
+ ) => {
70
+ const actionRefComponent = useRef<ActionType>();
71
+ const actionRef = actionRefProp || actionRefComponent;
72
+ // const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
73
+ const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
74
+ const [editableData, setEditableData] = useState<(Entity)[]>([]);
75
+ const intl = useIntl();
76
+
77
+ useEffect(() => {
78
+ actionRef?.current?.reload();
79
+ }, [JSON.stringify(pathParams)]);
80
+
81
+ const request = async (
82
+ params: TFilterParams,
83
+ sort: TSort = {},
84
+ filters: TFilters = {},
85
+ ) => {
86
+ const {
87
+ current,
88
+ pageSize,
89
+ keyword,
90
+ baseFilters,
91
+ join,
92
+ sortMap,
93
+ } = params;
94
+
95
+ const queryParams: TGetAllParams & TPathParams = {
96
+ ...pathParams,
97
+ page: current,
98
+ limit: pageSize,
99
+ };
100
+
101
+ const sortBy = Object
102
+ .entries(sort)
103
+ .reduce<string[]>(
104
+ (data: string[], [key, direction]) => {
105
+ data.push(`${sortMap?.[key] || key},${direction === 'ascend' ? 'ASC' : 'DESC'}`);
106
+ return data;
107
+ },
108
+ []
109
+ );
110
+ if (!sortBy.length && defaultSort) {
111
+ sortBy.push(defaultSort.join(','));
112
+ }
113
+ queryParams.sort = sortBy;
114
+
115
+ let search = getFiltersSearch({
116
+ baseFilters,
117
+ filters,
118
+ searchableColumns,
119
+ });
120
+ search = applyKeywordToSearch(search, searchableColumns!, null, keyword);
121
+ queryParams.s = JSON.stringify(search);
122
+
123
+ const { joinSelect, joinFields } = buildJoinFields(join);
124
+ queryParams.join = joinSelect;
125
+
126
+ queryParams.fields = columns && collectFieldsFromColumns(
127
+ columns,
128
+ idColumnName,
129
+ joinFields,
130
+ ) || [];
131
+
132
+ return getAll(queryParams);
133
+ }
134
+
135
+ const createButton = <Button
136
+ size='middle'
137
+ type="primary"
138
+ key="create"
139
+ onClick={() => {
140
+ // if (popupCreation) {
141
+ // setCreatePopupData(createNewDefaultParams);
142
+ // } else {
143
+ const newId = getNewId();
144
+ actionRef?.current?.addEditRecord({
145
+ [idColumnName]: newId,
146
+ [KEY_SYMBOL]: newId,
147
+ ...createNewDefaultParams,
148
+ }, {
149
+ position: 'top',
150
+ });
151
+ // }
152
+ }}
153
+ >
154
+ <PlusOutlined /> <FormattedMessage id={'table.newButton'} />
155
+ </Button>;
156
+
157
+ return (<>
158
+ <ProList<Entity>
159
+ className={useStyles().styles.list}
160
+ actionRef={actionRef}
161
+ // todo: fix ts
162
+ // @ts-ignore
163
+ request={request}
164
+ rowKey={record => record[KEY_SYMBOL] ?? record[idColumnName]}
165
+ bordered
166
+ search={false}
167
+ options={{
168
+ fullScreen: true,
169
+ reload: true,
170
+ search: {
171
+ allowClear: true,
172
+ },
173
+ }}
174
+ editable={{
175
+ type: 'multiple',
176
+ editableKeys,
177
+ onChange: setEditableRowKeys,
178
+ onValuesChange(entity, data) {
179
+ setEditableData(data);
180
+ },
181
+ async onSave(
182
+ id,
183
+ record,
184
+ origin,
185
+ newLine,
186
+ ) {
187
+ const data = editableData.find(entity => entity[idColumnName] === id) || record;
188
+ if (newLine) {
189
+ await onCreate?.({
190
+ ...pathParams,
191
+ requestBody: entityToCreateDto(data)
192
+ });
193
+ } else {
194
+ await onUpdate({
195
+ ...pathParams,
196
+ ...{ [idColumnName]: String(id) } as Record<keyof Entity, string>,
197
+ requestBody: entityToUpdateDto({
198
+ ...pathParams,
199
+ ...data,
200
+ }),
201
+ })
202
+ }
203
+
204
+ if (typeof afterSave === 'function') {
205
+ await afterSave(data);
206
+ }
207
+
208
+ flushSync(() => {
209
+ actionRef?.current?.reload();
210
+ });
211
+ },
212
+ async onCancel(
213
+ id,
214
+ record,
215
+ origin,
216
+ ) {
217
+ editableData.forEach(entity => {
218
+ if (entity[idColumnName] === id) {
219
+ Object.assign(entity, origin);
220
+ }
221
+ })
222
+ },
223
+ async onDelete(id) {
224
+ await onDelete({ ...{ [idColumnName]: String(id) } as Record<keyof Entity, string>, ...pathParams });
225
+ },
226
+ deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
227
+ onlyAddOneLineAlertMessage: intl.formatMessage({ id: 'table.onlyAddOneLineAlertMessage' }),
228
+ cancelText: <Tooltip title={intl.formatMessage({ id: 'table.cancelText' })}><StopOutlined /></Tooltip>,
229
+ deleteText: <Tooltip title={intl.formatMessage({ id: 'table.deleteText' })}><DeleteOutlined /></Tooltip>,
230
+ saveText: <Button size={"small"} type={"primary"}><FormattedMessage id={'table.saveText'} /></Button>,
231
+ ...editable,
232
+ }}
233
+ toolBarRender={(...args) => [
234
+ ...toolBarRender && toolBarRender(...args) || [],
235
+ !viewOnly && createButton || null,
236
+ ]}
237
+ grid={{ gutter: 16, column: 1 }}
238
+ metas={metas}
239
+ {...rest}
240
+ />
241
+ {/*<DescriptionsCreateModal<Entity>*/}
242
+ {/* data={createPopupData}*/}
243
+ {/* onClose={() => setCreatePopupData(undefined)}*/}
244
+ {/* onSubmit={async (data) => {*/}
245
+ {/* try {*/}
246
+ {/* await onCreate?.({*/}
247
+ {/* ...pathParams,*/}
248
+ {/* requestBody: entityToCreateDto({*/}
249
+ {/* ...pathParams,*/}
250
+ {/* ...data,*/}
251
+ {/* })*/}
252
+ {/* });*/}
253
+ {/* actionRef?.current?.reload();*/}
254
+ {/* setCreatePopupData(undefined);*/}
255
+ {/* }*/}
256
+ {/* catch (e) {*/}
257
+ {/* console.error(e);*/}
258
+ {/* }*/}
259
+ {/* }}*/}
260
+ {/* idColumnName={idColumnName}*/}
261
+ {/* columns={columns ?? []}*/}
262
+ {/*/>*/}
263
+ </>);
264
+ };
265
+
266
+ export default List;
@@ -0,0 +1,3 @@
1
+ export * from './List';
2
+ export { default as List } from './List';
3
+ export * from './listTypes';
@@ -0,0 +1,31 @@
1
+ import { MutableRefObject } from "react";
2
+ import { ActionType } from "@ant-design/pro-table";
3
+ import { QuerySortArr } from "@nestjsx/crud-request";
4
+ import { RowEditableConfig } from "@ant-design/pro-utils";
5
+ import { ProListProps } from "@ant-design/pro-components";
6
+ import { TFilterParams, TGetAllParams, TSearchableColumn } from "../Table";
7
+
8
+ export interface TListProps<
9
+ Entity,
10
+ CreateDto,
11
+ UpdateDto,
12
+ TEntityParams = {},
13
+ TPathParams = {}
14
+ > extends ProListProps<Entity, TEntityParams & TFilterParams> {
15
+ getAll: ({}: TGetAllParams & TPathParams) => Promise<{ data: Entity[] }>,
16
+ onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>,
17
+ onUpdate: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>,
18
+ onDelete: ({}: Record<keyof Entity, string> & TPathParams) => Promise<void>,
19
+ pathParams: TPathParams,
20
+ idColumnName?: string & keyof Entity,
21
+ entityToCreateDto: (entity: Entity) => CreateDto,
22
+ entityToUpdateDto: (entity: Entity) => UpdateDto,
23
+ createNewDefaultParams?: Partial<Entity>,
24
+ afterSave?: (record: Entity) => Promise<void>,
25
+ actionRef?: MutableRefObject<ActionType | undefined>,
26
+ editable?: RowEditableConfig<Entity>,
27
+ defaultSort?: QuerySortArr,
28
+ searchableColumns?: TSearchableColumn[],
29
+ popupCreation?: boolean,
30
+ viewOnly?: boolean,
31
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { Popover } from "antd";
3
+ import { QuestionCircleTwoTone } from "@ant-design/icons";
4
+ import { PrimitiveType } from "intl-messageformat";
5
+ import { useIntl } from "react-intl";
6
+
7
+ export const QuestionMarkHint: React.FC<{
8
+ intlPrefix: string,
9
+ values?: Record<string, PrimitiveType | React.ReactElement>
10
+ }> = ({
11
+ intlPrefix,
12
+ values,
13
+ }) => {
14
+ const intl = useIntl();
15
+ return (
16
+ <Popover
17
+ content={(
18
+ <div
19
+ style={{
20
+ maxWidth: 300,
21
+ }}
22
+ >{intl.formatMessage({ id: `${intlPrefix}.hint.message` }, values)}</div>
23
+ )}
24
+ title={intl.formatMessage({ id: `${intlPrefix}.hint.title` })}
25
+ trigger={['hover', 'click']}
26
+ zIndex={1080}
27
+ >
28
+ <QuestionCircleTwoTone />
29
+ </Popover>
30
+ )
31
+ }
32
+
33
+ export default QuestionMarkHint;
@@ -0,0 +1 @@
1
+ export * from './QuestionMarkHint';
@@ -0,0 +1,55 @@
1
+ import { Button, Popconfirm } from "antd";
2
+ import { LoadingOutlined } from "@ant-design/icons";
3
+ import { TGetAllParams } from "./tableTypes";
4
+ import { createStyles } from "antd-style";
5
+ import { useState } from "react";
6
+
7
+ const useStyles = createStyles(() => {
8
+ return {
9
+ popconfirm: {
10
+ '.ant-popconfirm-description': {
11
+ marginTop: '0 !important',
12
+ },
13
+ }
14
+ }
15
+ })
16
+
17
+ const BulkDeleteButton = <Entity extends Record<string | symbol, any>>(
18
+ {
19
+ selectedRecords,
20
+ lastRequest,
21
+ allSelected,
22
+ onDelete,
23
+ } : {
24
+ selectedRecords: Entity[],
25
+ allSelected: boolean,
26
+ lastRequest: [TGetAllParams & Record<string, string | number>, any] | [],
27
+ onDelete: () => Promise<void>
28
+ }) => {
29
+ const { styles } = useStyles();
30
+ const [loading, setLoading] = useState(false);
31
+ const recordsCount = allSelected ? lastRequest[1].total : selectedRecords.length;
32
+
33
+ return (<>
34
+ <Popconfirm
35
+ overlayClassName={styles.popconfirm}
36
+ title={false}
37
+ description={`Are you sure you want to delete ${recordsCount} ${recordsCount === 1 ? 'record' : 'records'}?`}
38
+ onConfirm={() => {
39
+ setLoading(true);
40
+ onDelete().finally(() => setLoading(false));
41
+ }}
42
+ okText="Yes"
43
+ cancelText="No"
44
+ >
45
+ <Button
46
+ disabled={recordsCount === 0}
47
+ >
48
+ {recordsCount > 0 ? `Delete ${recordsCount} ${recordsCount === 1 ? 'Record' : 'Records'}` : 'Bulk Delete'}
49
+ {loading && <LoadingOutlined />}
50
+ </Button>
51
+ </Popconfirm>
52
+ </>);
53
+ };
54
+
55
+ export default BulkDeleteButton;