@boarteam/boar-pack-common-frontend 2.9.0 → 3.0.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 +4 -3
- package/src/components/Comment/Comment.tsx +53 -0
- package/src/components/Comment/CommentAvatar.tsx +34 -0
- package/src/components/Comment/CommentForm.tsx +36 -0
- package/src/components/Comment/CommentFormModal.tsx +31 -0
- package/src/components/Comment/index.ts +5 -0
- package/src/components/Table/Table.tsx +2 -0
- package/src/components/Table/tableTools.ts +20 -4
- package/src/components/Table/tableTypes.ts +5 -1
- package/src/components/Table/useColumnsSets.tsx +39 -7
- package/src/components/Table/useCreation.tsx +2 -2
- package/src/components/Table/useImportExport.tsx +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boarteam/boar-pack-common-frontend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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>",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@nestjsx/crud-request": "^5.0.0-alpha.3",
|
|
29
|
-
"lodash": "^4.17.21"
|
|
29
|
+
"lodash": "^4.17.21",
|
|
30
|
+
"uuid": "^11.1.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@ant-design/icons": "^4.8.3",
|
|
@@ -46,5 +47,5 @@
|
|
|
46
47
|
"scripts": {
|
|
47
48
|
"yalc:push": "yalc push"
|
|
48
49
|
},
|
|
49
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "36339bec16a3d1966bab8df4e10f3fa0ad49f4e4"
|
|
50
51
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import { createStyles } from "antd-style";
|
|
3
|
+
import CommentAvatar from "./CommentAvatar";
|
|
4
|
+
|
|
5
|
+
export interface AuthorProps {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CommentProps {
|
|
11
|
+
key: string;
|
|
12
|
+
content: string;
|
|
13
|
+
author: AuthorProps;
|
|
14
|
+
date: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const useStyles = createStyles(() => {
|
|
18
|
+
return {
|
|
19
|
+
/**
|
|
20
|
+
* Styles for the ant-descriptions component to show edit icon on hover
|
|
21
|
+
*/
|
|
22
|
+
commentStyles: {
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'flex-start',
|
|
25
|
+
gap: '10px'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const Comment: React.FC<CommentProps> = ({
|
|
31
|
+
content,
|
|
32
|
+
author,
|
|
33
|
+
date,
|
|
34
|
+
...rest
|
|
35
|
+
}) => {
|
|
36
|
+
const { styles } = useStyles();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={styles.commentStyles}
|
|
41
|
+
{...rest}
|
|
42
|
+
>
|
|
43
|
+
<CommentAvatar author={author} />
|
|
44
|
+
<div>
|
|
45
|
+
<strong>{author.name}</strong>
|
|
46
|
+
<p>{content}</p>
|
|
47
|
+
<small style={{ color: '#888' }}>{dayjs(date).format('DD.MM.YYYY HH:mm')}</small>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default Comment;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Avatar } from "antd";
|
|
2
|
+
import { AuthorProps } from "./Comment";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
interface CommentProps {
|
|
6
|
+
author: AuthorProps;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ColorList = [
|
|
10
|
+
'#f56a00',
|
|
11
|
+
'#7265e6',
|
|
12
|
+
'#ffbf00',
|
|
13
|
+
'#00a2ae',
|
|
14
|
+
'#b45d7e',
|
|
15
|
+
'#ace665',
|
|
16
|
+
'#6e3aaf',
|
|
17
|
+
'#54ae00'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const getColorByAuthor = (authorId: string) => {
|
|
21
|
+
return ColorList[parseInt(authorId, 36) % ColorList.length]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CommentAvatar: React.FC<CommentProps> = ({
|
|
25
|
+
author,
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<Avatar style={{ backgroundColor: getColorByAuthor(author.id), verticalAlign: 'middle', flexShrink: 0 }} size="large">
|
|
29
|
+
{author.name.slice(0, 1)}
|
|
30
|
+
</Avatar>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default CommentAvatar;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Form, Input, Button } from 'antd';
|
|
3
|
+
|
|
4
|
+
interface CommentFormProps {
|
|
5
|
+
onSubmit: (content: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CommentForm: React.FC<CommentFormProps> = ({ onSubmit }) => {
|
|
9
|
+
const [content, setContent] = useState('');
|
|
10
|
+
|
|
11
|
+
const handleSubmit = () => {
|
|
12
|
+
if (content.trim()) {
|
|
13
|
+
onSubmit(content);
|
|
14
|
+
setContent('');
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Form layout="vertical" onFinish={handleSubmit}>
|
|
20
|
+
<div>
|
|
21
|
+
<Form.Item label="Leave a comment:">
|
|
22
|
+
<Input.TextArea
|
|
23
|
+
value={content}
|
|
24
|
+
onChange={(e) => setContent(e.target.value)}
|
|
25
|
+
autoSize={{ minRows: 3, maxRows: 6 }}
|
|
26
|
+
/>
|
|
27
|
+
</Form.Item>
|
|
28
|
+
</div>
|
|
29
|
+
<Button type="primary" htmlType="submit" style={{ width: 100 }}>
|
|
30
|
+
Send
|
|
31
|
+
</Button>
|
|
32
|
+
</Form>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default CommentForm;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal } from 'antd';
|
|
3
|
+
import CommentForm from "./CommentForm";
|
|
4
|
+
|
|
5
|
+
interface CommentFormProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
setIsOpen: (open: boolean) => void;
|
|
8
|
+
onSubmit: (content: string) => void;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CommentFormModal: React.FC<CommentFormProps> = ({ setIsOpen, isOpen, onSubmit, children, title = 'Add comment' }) => {
|
|
14
|
+
return (
|
|
15
|
+
<Modal
|
|
16
|
+
title={title}
|
|
17
|
+
open={isOpen}
|
|
18
|
+
width={800}
|
|
19
|
+
closeIcon={true}
|
|
20
|
+
footer={null}
|
|
21
|
+
onCancel={() => {
|
|
22
|
+
setIsOpen(false);
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
<CommentForm onSubmit={onSubmit} />
|
|
27
|
+
</Modal>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default CommentFormModal;
|
|
@@ -36,6 +36,7 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
36
36
|
onDelete,
|
|
37
37
|
onDeleteMany,
|
|
38
38
|
exportUrl,
|
|
39
|
+
exportParams,
|
|
39
40
|
onImport,
|
|
40
41
|
pathParams,
|
|
41
42
|
idColumnName = 'id',
|
|
@@ -123,6 +124,7 @@ const Table = <Entity extends Record<string | symbol, any>,
|
|
|
123
124
|
|
|
124
125
|
const { exportButton, importButton, setLastQueryParams } = useImportExport<TPathParams>({
|
|
125
126
|
exportUrl,
|
|
127
|
+
exportParams,
|
|
126
128
|
onImport,
|
|
127
129
|
})
|
|
128
130
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CondOperator, QueryJoin, SCondition } from "@nestjsx/crud-request";
|
|
2
|
+
import { validate as uuidValidate } from 'uuid';
|
|
2
3
|
import { IWithId, TFilters, TSearchableColumn } from "./tableTypes";
|
|
3
4
|
import React, { Key } from "react";
|
|
4
5
|
import { TColumnsStates } from "./useColumnsSets";
|
|
@@ -20,7 +21,12 @@ export function getFiltersSearch({
|
|
|
20
21
|
let operator = col.filterOperator || col.operator;
|
|
21
22
|
let value = filters[colDataIndex] || baseFilters[colDataIndex];
|
|
22
23
|
filterKeys.delete(colDataIndex);
|
|
23
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
value === '' ||
|
|
26
|
+
value === undefined ||
|
|
27
|
+
col.numeric && !Number.isFinite(Number(value)) ||
|
|
28
|
+
col.uuid && (typeof value !== 'string' || !uuidValidate(value))
|
|
29
|
+
) {
|
|
24
30
|
return;
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -126,7 +132,10 @@ export function applyKeywordToSearch(
|
|
|
126
132
|
const field = col.searchField || (Array.isArray(col.field) ? col.field.join('.') : col.field);
|
|
127
133
|
const operator = col.operator;
|
|
128
134
|
|
|
129
|
-
|
|
135
|
+
const wrongNumeric = col.numeric && !Number.isFinite(Number(word));
|
|
136
|
+
const wrongUuid = col.uuid && (typeof word !== 'string' || !uuidValidate(word));
|
|
137
|
+
|
|
138
|
+
if (!wrongNumeric && !wrongUuid) {
|
|
130
139
|
keywordSearch.$or.push({ [field]: { [operator]: word } });
|
|
131
140
|
}
|
|
132
141
|
});
|
|
@@ -146,6 +155,7 @@ export function applyKeywordToSearch(
|
|
|
146
155
|
export type TIndexableRecord = {
|
|
147
156
|
dataIndex?: Key | Key[];
|
|
148
157
|
children?: TIndexableRecord[] | React.ReactNode;
|
|
158
|
+
editable?: false;
|
|
149
159
|
};
|
|
150
160
|
|
|
151
161
|
export function collectFieldsFromColumns<T>(
|
|
@@ -166,7 +176,9 @@ export function buildFieldsFromColumnsForDescriptionsDisplay<T>(
|
|
|
166
176
|
if ('children' in col && Array.isArray(col.children)) {
|
|
167
177
|
buildFieldsFromColumnsForDescriptionsDisplay(col.children, idColumnName, fields);
|
|
168
178
|
}
|
|
169
|
-
|
|
179
|
+
if (col.editable !== false) {
|
|
180
|
+
fields.add(String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex));
|
|
181
|
+
}
|
|
170
182
|
});
|
|
171
183
|
|
|
172
184
|
return fields;
|
|
@@ -186,7 +198,11 @@ export function buildFieldsFromColumns<T>(
|
|
|
186
198
|
// skip id column because it is always included by backend
|
|
187
199
|
// and join fields because they are included by join
|
|
188
200
|
|
|
189
|
-
const dataIndex =
|
|
201
|
+
const dataIndex = Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex;
|
|
202
|
+
if (typeof dataIndex !== 'string') {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
if (!dataIndex || (Array.isArray(idColumnName) ? idColumnName.includes(dataIndex) : dataIndex === idColumnName) || joinFields.has(dataIndex)) {
|
|
191
207
|
return;
|
|
192
208
|
}
|
|
@@ -53,7 +53,7 @@ export type TGetAllParams = {
|
|
|
53
53
|
cache?: number,
|
|
54
54
|
}
|
|
55
55
|
export type TFilters = {
|
|
56
|
-
[key: string]: number | string | boolean | (string | number)[] | null;
|
|
56
|
+
[key: string]: number | string | boolean | (string | number | boolean)[] | null;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export type TGetRequestParams = {
|
|
@@ -78,6 +78,7 @@ export type TSearchableColumn = {
|
|
|
78
78
|
filterField?: string,
|
|
79
79
|
filterOperator?: typeof Operators[keyof typeof Operators],
|
|
80
80
|
numeric?: boolean,
|
|
81
|
+
uuid?: boolean,
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
interface BaseProps<Entity,
|
|
@@ -108,6 +109,9 @@ export interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
|
|
|
108
109
|
afterSave?: (record: Entity) => Promise<void>;
|
|
109
110
|
onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
|
|
110
111
|
exportUrl?: string;
|
|
112
|
+
exportParams?: {
|
|
113
|
+
[key: string]: string | number
|
|
114
|
+
};
|
|
111
115
|
onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
|
|
112
116
|
onUpdate: ({}: Partial<Entity> & {
|
|
113
117
|
requestBody: UpdateDto,
|
|
@@ -33,19 +33,28 @@ function getColumnsStates<T>(
|
|
|
33
33
|
columns: TIndexableRecord[],
|
|
34
34
|
shownCols: Set<keyof T>,
|
|
35
35
|
state: TColumnsState = {},
|
|
36
|
-
): Record<string, ColumnsState
|
|
36
|
+
): {state: Record<string, ColumnsState>, someColumnsShown: boolean} {
|
|
37
|
+
let someColumnsShown = false;
|
|
37
38
|
columns.forEach(col => {
|
|
38
39
|
const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex;
|
|
40
|
+
let childrenColumnsShown = false;
|
|
39
41
|
if ('children' in col && Array.isArray(col.children)) {
|
|
40
|
-
getColumnsStates(col.children, shownCols, state);
|
|
42
|
+
const { someColumnsShown } = getColumnsStates(col.children, shownCols, state);
|
|
43
|
+
if (someColumnsShown) {
|
|
44
|
+
childrenColumnsShown = true;
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
if (idx
|
|
44
|
-
|
|
48
|
+
if (idx) {
|
|
49
|
+
if (shownCols.has(idx as keyof T) || childrenColumnsShown) {
|
|
50
|
+
someColumnsShown = true;
|
|
51
|
+
} else {
|
|
52
|
+
state[idx as string] = { show: false };
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
}, state);
|
|
47
56
|
|
|
48
|
-
return state
|
|
57
|
+
return { state, someColumnsShown };
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export default function useColumnsSets<Entity>({
|
|
@@ -60,7 +69,7 @@ export default function useColumnsSets<Entity>({
|
|
|
60
69
|
columns: columnsSet
|
|
61
70
|
}) => [
|
|
62
71
|
name,
|
|
63
|
-
getColumnsStates<Entity>(columns, new Set(columnsSet))
|
|
72
|
+
getColumnsStates<Entity>(columns as TIndexableRecord[], new Set(columnsSet)).state,
|
|
64
73
|
])
|
|
65
74
|
), [columns]
|
|
66
75
|
);
|
|
@@ -97,7 +106,30 @@ export default function useColumnsSets<Entity>({
|
|
|
97
106
|
|
|
98
107
|
const columnsState = {
|
|
99
108
|
value: chosenColumnsSet,
|
|
100
|
-
|
|
109
|
+
// value contains only hidden columns and one which is just changed
|
|
110
|
+
onChange: (value: TColumnsStates) => {
|
|
111
|
+
const checkParentVisibility = (columns: TIndexableRecord[]) => {
|
|
112
|
+
let someColumnsShown = false;
|
|
113
|
+
|
|
114
|
+
columns.forEach(col => {
|
|
115
|
+
const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex as string;
|
|
116
|
+
if (idx && value[idx]?.show) {
|
|
117
|
+
someColumnsShown = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if ('children' in col && Array.isArray(col.children)) {
|
|
121
|
+
const someChildColumnsShown = checkParentVisibility(col.children);
|
|
122
|
+
if (someChildColumnsShown) {
|
|
123
|
+
value[idx] = { show: true };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return someColumnsShown;
|
|
129
|
+
};
|
|
130
|
+
checkParentVisibility(columns as TIndexableRecord[]);
|
|
131
|
+
setChosenColumnsSet(value);
|
|
132
|
+
},
|
|
101
133
|
};
|
|
102
134
|
|
|
103
135
|
return {
|
|
@@ -35,7 +35,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
|
|
|
35
35
|
}: {
|
|
36
36
|
actionRef?: MutableRefObject<ActionType | undefined>;
|
|
37
37
|
pathParams: TPathParams;
|
|
38
|
-
entityToCreateDto: (entity:
|
|
38
|
+
entityToCreateDto: (entity: Entity) => CreateDto;
|
|
39
39
|
onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
|
|
40
40
|
createButtonSize: SizeType;
|
|
41
41
|
popupCreation?: boolean;
|
|
@@ -43,7 +43,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
|
|
|
43
43
|
} & Omit<CreateEntityModalProps<Entity>, 'onSubmit' | 'onCancel' | 'entity'>) {
|
|
44
44
|
const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
|
|
45
45
|
|
|
46
|
-
const onCreateSubmit = async (data:
|
|
46
|
+
const onCreateSubmit = async (data: Entity, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
|
|
47
47
|
try {
|
|
48
48
|
await onCreate?.({
|
|
49
49
|
...pathParams,
|
|
@@ -6,9 +6,13 @@ import { Link } from "react-router-dom";
|
|
|
6
6
|
|
|
7
7
|
export function useImportExport<TPathParams = {}>({
|
|
8
8
|
exportUrl,
|
|
9
|
+
exportParams,
|
|
9
10
|
onImport
|
|
10
11
|
}: {
|
|
11
12
|
exportUrl?: string;
|
|
13
|
+
exportParams?: {
|
|
14
|
+
[key: string]: string | number
|
|
15
|
+
}
|
|
12
16
|
onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
|
|
13
17
|
}) {
|
|
14
18
|
const [isLoadingImport, setIsLoadingImport] = useState(false);
|
|
@@ -25,10 +29,15 @@ export function useImportExport<TPathParams = {}>({
|
|
|
25
29
|
});
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const params = {
|
|
33
|
+
...(lastQueryParams && {
|
|
34
|
+
s: lastQueryParams.s,
|
|
35
|
+
sort: lastQueryParams.sort?.[0],
|
|
36
|
+
}),
|
|
37
|
+
...exportParams
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = exportUrl + (Object.keys(params).length ? '?' + new URLSearchParams(params).toString() : '');
|
|
32
41
|
const exportButton = <Tooltip title="Export">
|
|
33
42
|
<Link to={url} target={'_blank'}>
|
|
34
43
|
<Button icon={<DownloadOutlined />}/>
|