@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boarteam/boar-pack-common-frontend",
3
- "version": "2.9.0",
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": "e6e2920ef9786ec27cc616c35796f64f6183a3f5"
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;
@@ -0,0 +1,5 @@
1
+ export * from './Comment';
2
+ export * from './CommentAvatar';
3
+ export * from './CommentForm';
4
+ export * from './CommentFormModal';
5
+ export { default as Comment } from './Comment';
@@ -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 (value === '' || value === undefined || col.numeric && !Number.isFinite(Number(value))) {
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
- if (!col.numeric || Number.isFinite(Number(word))) {
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
- fields.add(String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex));
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 = String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.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 && !shownCols.has(idx as keyof T)) {
44
- state[idx as string] = { show: false };
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 as Record<string, ColumnsState>;
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
- onChange: setChosenColumnsSet,
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: Partial<Entity>) => CreateDto;
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: Partial<Entity>, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
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 url = exportUrl + (lastQueryParams ? '?' + new URLSearchParams({
29
- s: lastQueryParams.s,
30
- sort: lastQueryParams.sort?.[0],
31
- }).toString() : '');
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 />}/>