@boarteam/boar-pack-common-frontend 2.10.0 → 3.1.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.10.0",
3
+ "version": "3.1.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": "103534abbe12f409d96c929dd462837ec83b4ee7"
50
+ "gitHead": "008e1d42d47532005e1af1909d40befdebff5197"
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';
@@ -1,6 +1,6 @@
1
1
  import { ActionType } from "@ant-design/pro-table";
2
2
  import React, { Key, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
3
- import { Badge, Button, Result, Tabs, TabsProps, Tooltip } from "antd";
3
+ import { Badge, Button, Form, Result, Tabs, TabsProps, Tooltip } from "antd";
4
4
  import { DeleteOutlined, StopOutlined } from "@ant-design/icons";
5
5
  import { FormattedMessage, useIntl } from "react-intl";
6
6
  import { DescriptionsRefType, FieldsEdit, TDescriptionsProps, TGetOneParams } from "./descriptionTypes";
@@ -21,7 +21,7 @@ import { debounce } from "lodash";
21
21
  import { NamePath } from "antd/lib/form/interface";
22
22
  import { FieldData } from "rc-field-form/lib/interface";
23
23
 
24
- const useStyles = createStyles(({css}) => {
24
+ const useStyles = createStyles(({ css }) => {
25
25
  return {
26
26
  antDescriptionsStyles: css`
27
27
  .ant-descriptions-item-content {
@@ -62,6 +62,7 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
62
62
  columns,
63
63
  params,
64
64
  onEntityChange,
65
+ conditionalFieldsConfig,
65
66
  ...rest
66
67
  }: TDescriptionsProps<Entity,
67
68
  CreateDto,
@@ -80,13 +81,36 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
80
81
  }
81
82
  form = editable.form;
82
83
 
84
+ const watchedValue = conditionalFieldsConfig
85
+ ? Form.useWatch(conditionalFieldsConfig.field, form)
86
+ : undefined;
87
+
88
+ const filteredColumns = React.useMemo(() => {
89
+ if (!conditionalFieldsConfig) {
90
+ return columns;
91
+ }
92
+
93
+ const { field, deps } = conditionalFieldsConfig;
94
+ const depsList = deps[watchedValue as string];
95
+
96
+ if (!depsList?.length) {
97
+ return columns;
98
+ }
99
+
100
+ // Always include the watched field itself
101
+ return columns.filter(col => {
102
+ const idx = col.dataIndex as string;
103
+ return idx === field || depsList.includes(idx);
104
+ });
105
+ }, [columns, watchedValue, conditionalFieldsConfig]);
106
+
83
107
  const actionRefComponent = useRef<ActionType>();
84
108
  const actionRef = actionRefProp || actionRefComponent;
85
109
  const intl = useIntl();
86
110
  const [data, setData] = useState<Partial<Entity> | undefined>(entity);
87
111
  const [loading, setLoading] = useState(false);
88
112
 
89
- const sections = columnsToDescriptionItemProps(columns, mainTitle);
113
+ const sections = columnsToDescriptionItemProps(filteredColumns, mainTitle);
90
114
 
91
115
  const columnDataIndexToSection = sections.reduce((acc, section) => {
92
116
  section.columns.forEach(column => {
@@ -35,7 +35,15 @@ export enum FieldsEdit {
35
35
  All = 'all'
36
36
  }
37
37
 
38
+ export interface ConditionalFieldsConfig {
39
+ /** Which field to watch */
40
+ field: string;
41
+ /** Map of watched-value → array of dataIndex you want to show */
42
+ deps: Record<string, string[]>;
43
+ }
44
+
38
45
  export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
46
+ conditionalFieldsConfig?: ConditionalFieldsConfig;
39
47
  mainTitle?: ProColumns<Entity>['title'] | null,
40
48
  entity?: Partial<Entity>,
41
49
  getOne?: ({}: TGetOneParams & TPathParams) => Promise<Entity | null>,
@@ -1,7 +1,7 @@
1
1
  import { ProColumns } from "@ant-design/pro-components";
2
2
  import { Button, Modal } from "antd";
3
3
  import { MutableRefObject, useRef } from "react";
4
- import { Descriptions, DescriptionsRefType } from "../Descriptions";
4
+ import { ConditionalFieldsConfig, Descriptions, DescriptionsRefType } from "../Descriptions";
5
5
  import { buildFieldsFromColumnsForDescriptionsDisplay } from "./tableTools";
6
6
 
7
7
  export interface CreateEntityModalProps<Entity> {
@@ -24,6 +24,8 @@ export interface CreateEntityModalProps<Entity> {
24
24
  * Receives the validated form data.
25
25
  */
26
26
  onSubmit: (data: any, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => Promise<void>;
27
+ /** Configuration for conditional fields */
28
+ conditionalFieldsConfig?: ConditionalFieldsConfig
27
29
  }
28
30
 
29
31
  export function CreateEntityModal<
@@ -40,6 +42,7 @@ export function CreateEntityModal<
40
42
  idColumnName,
41
43
  onCancel,
42
44
  onSubmit,
45
+ conditionalFieldsConfig,
43
46
  }: CreateEntityModalProps<Entity>) {
44
47
  const descriptionsRef = useRef<DescriptionsRefType<Entity>>(null);
45
48
 
@@ -79,6 +82,7 @@ export function CreateEntityModal<
79
82
  editableKeys,
80
83
  actionRender: () => [],
81
84
  }}
85
+ conditionalFieldsConfig={conditionalFieldsConfig}
82
86
  />
83
87
  </Modal>
84
88
  );
@@ -55,6 +55,7 @@ const Table = <Entity extends Record<string | symbol, any>,
55
55
  columnsState: managedColumnsState,
56
56
  columnsSetSelect: managedColumnsSetSelect,
57
57
  popupCreation = false,
58
+ conditionalFieldsConfig,
58
59
  toolBarRender,
59
60
  params,
60
61
  editPopupTitle,
@@ -120,6 +121,7 @@ const Table = <Entity extends Record<string | symbol, any>,
120
121
  createButtonSize: rest.size,
121
122
  popupCreation,
122
123
  createNewDefaultParams,
124
+ conditionalFieldsConfig,
123
125
  });
124
126
 
125
127
  const { exportButton, importButton, setLastQueryParams } = useImportExport<TPathParams>({
@@ -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
  }
@@ -6,6 +6,7 @@ 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
8
  import { ProColumns } from "@ant-design/pro-components";
9
+ import { ConditionalFieldsConfig } from "../Descriptions";
9
10
 
10
11
  export type IWithId = {
11
12
  id: string | number,
@@ -53,7 +54,7 @@ export type TGetAllParams = {
53
54
  cache?: number,
54
55
  }
55
56
  export type TFilters = {
56
- [key: string]: number | string | boolean | (string | number)[] | null;
57
+ [key: string]: number | string | boolean | (string | number | boolean)[] | null;
57
58
  }
58
59
 
59
60
  export type TGetRequestParams = {
@@ -78,6 +79,7 @@ export type TSearchableColumn = {
78
79
  filterField?: string,
79
80
  filterOperator?: typeof Operators[keyof typeof Operators],
80
81
  numeric?: boolean,
82
+ uuid?: boolean,
81
83
  }
82
84
 
83
85
  interface BaseProps<Entity,
@@ -95,6 +97,7 @@ interface BaseProps<Entity,
95
97
  viewOnly?: boolean;
96
98
  columnsSets?: TColumnsSet<Entity>[];
97
99
  popupCreation?: boolean;
100
+ conditionalFieldsConfig?: ConditionalFieldsConfig;
98
101
  columnsState?: ColumnStateType;
99
102
  columnsSetSelect?: () => React.ReactNode;
100
103
  editPopupTitle?: string;
@@ -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 {
@@ -32,10 +32,11 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
32
32
  createButtonSize,
33
33
  popupCreation,
34
34
  createNewDefaultParams,
35
+ conditionalFieldsConfig,
35
36
  }: {
36
37
  actionRef?: MutableRefObject<ActionType | undefined>;
37
38
  pathParams: TPathParams;
38
- entityToCreateDto: (entity: Partial<Entity>) => CreateDto;
39
+ entityToCreateDto: (entity: Entity) => CreateDto;
39
40
  onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
40
41
  createButtonSize: SizeType;
41
42
  popupCreation?: boolean;
@@ -43,7 +44,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
43
44
  } & Omit<CreateEntityModalProps<Entity>, 'onSubmit' | 'onCancel' | 'entity'>) {
44
45
  const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
45
46
 
46
- const onCreateSubmit = async (data: Partial<Entity>, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
47
+ const onCreateSubmit = async (data: Entity, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
47
48
  try {
48
49
  await onCreate?.({
49
50
  ...pathParams,
@@ -105,6 +106,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
105
106
  setCreatePopupData(undefined);
106
107
  }}
107
108
  onSubmit={onCreateSubmit}
109
+ conditionalFieldsConfig={conditionalFieldsConfig}
108
110
  />;
109
111
 
110
112
  return {