@boarteam/boar-pack-common-frontend 2.6.0-alpha.0 → 2.6.0-alpha.3

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.6.0-alpha.0",
3
+ "version": "2.6.0-alpha.3",
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>",
@@ -46,5 +46,5 @@
46
46
  "scripts": {
47
47
  "yalc:push": "yalc push"
48
48
  },
49
- "gitHead": "38ccd74e5ec92322ac4efaf3482b131d51fca296"
49
+ "gitHead": "8e43d50ded7a9d9554b6624c89dc8a038f82c8d2"
50
50
  }
@@ -15,26 +15,24 @@ import useContentViewMode, { VIEW_MODE_TYPE } from "./useContentViewMode";
15
15
  import { createStyles } from "antd-style";
16
16
  import { debounce } from "lodash";
17
17
  import { NamePath } from "antd/lib/form/interface";
18
+ import { FieldData } from "rc-field-form/lib/interface";
18
19
 
19
- const useStyles = createStyles(() => {
20
+ const useStyles = createStyles(({css}) => {
20
21
  return {
21
- /**
22
- * Styles for the ant-descriptions component to show edit icon on hover
23
- */
24
- antDescriptionsStyles: {
25
- '.anticon-edit': {
26
- opacity: 0,
27
- transition: 'opacity 200ms'
28
- },
29
- '.ant-descriptions-item-content': {
30
- width: '20%',
31
- },
32
- '.ant-descriptions-item-content:hover': {
33
- '.anticon-edit': {
34
- opacity: 1
35
- },
22
+ antDescriptionsStyles: css`
23
+ .ant-descriptions-item-content {
24
+ .anticon-edit {
25
+ opacity: 0;
26
+ transition: opacity 200ms;
27
+ }
28
+
29
+ &:hover {
30
+ .anticon-edit {
31
+ opacity: 1;
32
+ }
33
+ }
36
34
  }
37
- }
35
+ `
38
36
  }
39
37
  })
40
38
 
@@ -64,7 +62,7 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
64
62
  CreateDto,
65
63
  UpdateDto,
66
64
  TPathParams>,
67
- ref: React.Ref<DescriptionsRefType>,
65
+ ref: React.Ref<DescriptionsRefType<Entity>>,
68
66
  ) => {
69
67
  const { styles } = useStyles();
70
68
 
@@ -136,6 +134,9 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
136
134
  form.resetFields();
137
135
  },
138
136
  submit: () => handleSubmit(),
137
+ setFieldErrors: (fields: FieldData<Entity>[]) => {
138
+ form.setFields(fields)
139
+ }
139
140
  }));
140
141
 
141
142
  const onValuesChange = debounce((changedValues, allValues) => {
@@ -240,8 +241,7 @@ const DescriptionsComponent = <Entity extends Record<string | symbol, any>,
240
241
  <Result
241
242
  status="404"
242
243
  title="404"
243
- subTitle="The instrument is not found."
244
- extra={<Button type="primary" href={'/liquidity/ecn-instruments'}>See list of instruments</Button>}
244
+ subTitle="The entity is not found."
245
245
  />
246
246
  );
247
247
  }
@@ -4,6 +4,7 @@ import { RowEditableConfig } from "@ant-design/pro-utils";
4
4
  import { QueryJoin } from "@nestjsx/crud-request";
5
5
  import { ProColumns } from "@ant-design/pro-components";
6
6
  import { ProDescriptionsProps } from "@ant-design/pro-descriptions";
7
+ import { FieldData } from "rc-field-form/lib/interface";
7
8
 
8
9
  export type TGetOneParams = {
9
10
  /**
@@ -23,9 +24,10 @@ export type TDescriptionGetRequestParams = {
23
24
  join?: QueryJoin | QueryJoin[];
24
25
  };
25
26
 
26
- export type DescriptionsRefType = {
27
+ export type DescriptionsRefType<Entity> = {
27
28
  reset: () => void;
28
29
  submit: () => void;
30
+ setFieldErrors: (fields: FieldData<Entity>[]) => void;
29
31
  };
30
32
 
31
33
  export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = object> = {
@@ -49,7 +51,7 @@ export type TDescriptionsProps<Entity, CreateDto, UpdateDto, TPathParams = objec
49
51
  params?: TDescriptionGetRequestParams,
50
52
  columns: ProColumns<Entity>[],
51
53
  onEntityChange?: (entity: Entity | null) => void;
52
- ref?: React.Ref<DescriptionsRefType>,
54
+ ref?: React.Ref<DescriptionsRefType<Entity>>,
53
55
  } & Omit<ProDescriptionsProps<Entity>, 'columns'>;
54
56
 
55
57
  export type TDescriptionsCreateModalProps<Entity> = Omit<ProDescriptionsProps<Entity>, 'columns'> & {
@@ -1,6 +1,5 @@
1
- import { ProFormSelectProps } from "@ant-design/pro-form/lib/components/Select";
1
+ import { ProFormSelect, ProFormSelectProps } from "@ant-design/pro-components";
2
2
  import { useState } from "react";
3
- import { ProFormSelect } from "@ant-design/pro-form";
4
3
 
5
4
  type RelationSelectProps<T> = ProFormSelectProps & {
6
5
  selectedItem: T | null | undefined,
@@ -44,10 +43,8 @@ export const RelationSelect = function<T>({
44
43
  showSearch
45
44
  mode={'single'}
46
45
  request={request}
46
+ className='relational-select'
47
47
  formItemProps={{
48
- // correct color for invalid relational fields (#64)
49
- // @ts-ignore-next-line
50
- validateStatus: rest['aria-invalid'] === 'true' ? 'error' : 'success',
51
48
  style: {
52
49
  margin: 0,
53
50
  display: 'inline-block',
@@ -1,6 +1,6 @@
1
1
  import { ProColumns } from "@ant-design/pro-components";
2
2
  import { Button, Modal } from "antd";
3
- import { useRef } from "react";
3
+ import { MutableRefObject, useRef } from "react";
4
4
  import { Descriptions, DescriptionsRefType } from "../Descriptions";
5
5
  import { buildFieldsFromColumnsForDescriptionsDisplay } from "./tableTools";
6
6
 
@@ -23,7 +23,7 @@ export interface CreateEntityModalProps<Entity> {
23
23
  * Called when the form is submitted.
24
24
  * Receives the validated form data.
25
25
  */
26
- onSubmit: (data: any) => Promise<void>;
26
+ onSubmit: (data: any, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => Promise<void>;
27
27
  }
28
28
 
29
29
  export function CreateEntityModal<
@@ -41,7 +41,7 @@ export function CreateEntityModal<
41
41
  onCancel,
42
42
  onSubmit,
43
43
  }: CreateEntityModalProps<Entity>) {
44
- const descriptionsRef = useRef<DescriptionsRefType>(null);
44
+ const descriptionsRef = useRef<DescriptionsRefType<Entity>>(null);
45
45
 
46
46
  // Calculate the editable keys from the columns and idColumnName
47
47
  const editableKeys = [...buildFieldsFromColumnsForDescriptionsDisplay(columns, idColumnName)];
@@ -74,7 +74,7 @@ export function CreateEntityModal<
74
74
  labelStyle={{ width: '15%' }}
75
75
  contentStyle={{ width: '25%' }}
76
76
  canEdit={true}
77
- onCreate={onSubmit}
77
+ onCreate={(data) => onSubmit(data, descriptionsRef)}
78
78
  editable={{
79
79
  editableKeys,
80
80
  actionRender: () => [],
@@ -9,6 +9,7 @@ import { KEY_SYMBOL, useCreation } from "./useCreation";
9
9
  import { getTableDataQueryParams } from "./getTableDataQueryParams";
10
10
  import { useEditableTable } from "./useEditableTable";
11
11
  import { useBulkEditing } from "./useBulkEditing";
12
+ import { useImportExport } from "./useImportExport";
12
13
 
13
14
  const useStyles = createStyles(() => {
14
15
  return {
@@ -34,6 +35,8 @@ const Table = <Entity extends Record<string | symbol, any>,
34
35
  onUpdateMany,
35
36
  onDelete,
36
37
  onDeleteMany,
38
+ exportUrl,
39
+ onImport,
37
40
  pathParams,
38
41
  idColumnName = 'id',
39
42
  entityToCreateDto,
@@ -118,6 +121,11 @@ const Table = <Entity extends Record<string | symbol, any>,
118
121
  createNewDefaultParams,
119
122
  });
120
123
 
124
+ const { exportButton, importButton, setLastQueryParams } = useImportExport<TPathParams>({
125
+ exportUrl,
126
+ onImport,
127
+ })
128
+
121
129
  useEffect(() => {
122
130
  setUpdatePopupData(editableRecord);
123
131
  actionRef?.current?.reload();
@@ -158,6 +166,7 @@ const Table = <Entity extends Record<string | symbol, any>,
158
166
  queryParams,
159
167
  result,
160
168
  ]);
169
+ setLastQueryParams(queryParams);
161
170
  return result;
162
171
  }
163
172
 
@@ -181,10 +190,13 @@ const Table = <Entity extends Record<string | symbol, any>,
181
190
  listsHeight: 500,
182
191
  },
183
192
  }}
193
+ scroll={{
194
+ x: 'max-content',
195
+ }}
184
196
  bordered
185
197
  search={false}
186
198
  editable={editableConfig}
187
- toolBarRender={(...args) => [
199
+ toolBarRender={toolBarRender === false ? false : (...args) => [
188
200
  columnsSetSelect?.() || null,
189
201
  !viewOnly && onUpdateMany
190
202
  ? bulkEditButton
@@ -193,6 +205,8 @@ const Table = <Entity extends Record<string | symbol, any>,
193
205
  ? bulkDeleteButton
194
206
  : null,
195
207
  !viewOnly && createButton || null,
208
+ !viewOnly && onImport && importButton || null,
209
+ exportUrl && exportButton || null,
196
210
  ...toolBarRender && toolBarRender(...args) || [],
197
211
  ]}
198
212
  columns={columns}
@@ -58,6 +58,22 @@ export function getFiltersSearch({
58
58
  value = true;
59
59
  }
60
60
  break;
61
+
62
+ case Operators.in:
63
+ case Operators.inLow:
64
+ if (Array.isArray(value) && value.length === 1 && value[0] === null) {
65
+ operator = Operators.isNull;
66
+ value = true;
67
+ }
68
+ break;
69
+
70
+ case Operators.equals:
71
+ if (Array.isArray(value) && value.length === 1 && value[0] === null || value === null) {
72
+ operator = Operators.isNull;
73
+ value = true;
74
+ }
75
+ break;
76
+
61
77
  }
62
78
 
63
79
  search.$and?.push({ [field]: { [operator]: value } });
@@ -81,6 +97,7 @@ export const Operators = {
81
97
  lowerOrEquals: CondOperator.LOWER_THAN_EQUALS,
82
98
  isNull: CondOperator.IS_NULL,
83
99
  notNull: CondOperator.NOT_NULL,
100
+ starts: CondOperator.STARTS,
84
101
  } as const;
85
102
 
86
103
  export function applyKeywordToSearch(
@@ -172,7 +189,11 @@ export function buildFieldsFromColumns<T>(
172
189
  // skip id column because it is always included by backend
173
190
  // and join fields because they are included by join
174
191
 
175
- const dataIndex = String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex);
192
+ const dataIndex = Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex;
193
+ if (typeof dataIndex !== 'string') {
194
+ return;
195
+ }
196
+
176
197
  if (!dataIndex || (Array.isArray(idColumnName) ? idColumnName.includes(dataIndex) : dataIndex === idColumnName) || joinFields.has(dataIndex)) {
177
198
  return;
178
199
  }
@@ -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 = {
@@ -107,6 +107,8 @@ export interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
107
107
  editable?: RowEditableConfig<Entity>;
108
108
  afterSave?: (record: Entity) => Promise<void>;
109
109
  onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
110
+ exportUrl?: string;
111
+ onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
110
112
  onUpdate: ({}: Partial<Entity> & {
111
113
  requestBody: UpdateDto,
112
114
  index?: number,
@@ -33,19 +33,29 @@ 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
+ state[idx as string] = { show: true };
51
+ someColumnsShown = true;
52
+ } else {
53
+ state[idx as string] = { show: false };
54
+ }
45
55
  }
46
56
  }, state);
47
57
 
48
- return state as Record<string, ColumnsState>;
58
+ return { state, someColumnsShown };
49
59
  }
50
60
 
51
61
  export default function useColumnsSets<Entity>({
@@ -60,7 +70,7 @@ export default function useColumnsSets<Entity>({
60
70
  columns: columnsSet
61
71
  }) => [
62
72
  name,
63
- getColumnsStates<Entity>(columns, new Set(columnsSet))
73
+ getColumnsStates<Entity>(columns as TIndexableRecord[], new Set(columnsSet)).state,
64
74
  ])
65
75
  ), [columns]
66
76
  );
@@ -97,7 +107,30 @@ export default function useColumnsSets<Entity>({
97
107
 
98
108
  const columnsState = {
99
109
  value: chosenColumnsSet,
100
- onChange: setChosenColumnsSet,
110
+ // value contains only hidden columns and one which is just changed
111
+ onChange: (value: TColumnsStates) => {
112
+ const checkParentVisibility = (columns: TIndexableRecord[]) => {
113
+ let someColumnsShown = false;
114
+
115
+ columns.forEach(col => {
116
+ const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex as string;
117
+ if (idx && value[idx]?.show) {
118
+ someColumnsShown = true;
119
+ }
120
+
121
+ if ('children' in col && Array.isArray(col.children)) {
122
+ const someChildColumnsShown = checkParentVisibility(col.children);
123
+ if (someChildColumnsShown) {
124
+ value[idx] = { show: true };
125
+ }
126
+ }
127
+ });
128
+
129
+ return someColumnsShown;
130
+ };
131
+ checkParentVisibility(columns as TIndexableRecord[]);
132
+ setChosenColumnsSet(value);
133
+ },
101
134
  };
102
135
 
103
136
  return {
@@ -5,6 +5,8 @@ import { PlusOutlined } from "@ant-design/icons";
5
5
  import { FormattedMessage } from "react-intl";
6
6
  import type { SizeType } from "antd/es/config-provider/SizeContext";
7
7
  import { CreateEntityModal, CreateEntityModalProps } from "./CreateEntityModal";
8
+ import { DescriptionsRefType } from "../Descriptions";
9
+ import { ApiError } from '../../tools'
8
10
 
9
11
  let creatingRecordsCount = 0;
10
12
  export const KEY_SYMBOL = Symbol('key');
@@ -41,7 +43,7 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
41
43
  } & Omit<CreateEntityModalProps<Entity>, 'onSubmit' | 'onCancel' | 'entity'>) {
42
44
  const [createPopupData, setCreatePopupData] = useState<Partial<Entity> | undefined>();
43
45
 
44
- const onCreateSubmit = async (data: Entity) => {
46
+ const onCreateSubmit = async (data: Entity, descriptionsRef: MutableRefObject<DescriptionsRefType<Entity>>) => {
45
47
  try {
46
48
  await onCreate?.({
47
49
  ...pathParams,
@@ -54,6 +56,22 @@ export function useCreation<Entity, CreateDto, TPathParams = {}>({
54
56
  await actionRef?.current?.reload();
55
57
  } catch (e) {
56
58
  console.error(e);
59
+
60
+ // Handle common error
61
+ if (e.body && e.body.statusCode && e.body.errors) {
62
+ const error = e as ApiError;
63
+ const { statusCode, errors } = error.body;
64
+ // Validation error. Highlight corresponding form fields
65
+ if (statusCode === 400) {
66
+ const formErrors = errors.map(error => ({
67
+ name: error.field,
68
+ errors: [error.message],
69
+ }));
70
+
71
+ // @ts-ignore
72
+ descriptionsRef.current.setFieldErrors(formErrors);
73
+ }
74
+ }
57
75
  }
58
76
  };
59
77
 
@@ -5,6 +5,7 @@ import { Button, Tooltip } from "antd";
5
5
  import { DeleteOutlined, StopOutlined } from "@ant-design/icons";
6
6
  import { FormattedMessage, useIntl } from "react-intl";
7
7
  import React, { useState } from "react";
8
+ import { isRecordNew } from "./useCreation";
8
9
 
9
10
  export function useEditableTable<Entity, CreateDto, UpdateDto, TPathParams = {}>(
10
11
  {
@@ -68,6 +69,7 @@ export function useEditableTable<Entity, CreateDto, UpdateDto, TPathParams = {}>
68
69
  }
69
70
  },
70
71
  async onDelete(id, row) {
72
+ if (isRecordNew(row)) return;
71
73
  await onDelete({ ...row, ...pathParams });
72
74
  },
73
75
  deletePopconfirmMessage: intl.formatMessage({ id: 'table.deletePopconfirmMessage' }),
@@ -0,0 +1,58 @@
1
+ import { Button, Tooltip } from 'antd';
2
+ import { DownloadOutlined, UploadOutlined } from "@ant-design/icons";
3
+ import { useState } from "react";
4
+ import { TGetAllParams } from "./tableTypes";
5
+ import { Link } from "react-router-dom";
6
+
7
+ export function useImportExport<TPathParams = {}>({
8
+ exportUrl,
9
+ onImport
10
+ }: {
11
+ exportUrl?: string;
12
+ onImport?: (event: React.ChangeEvent<HTMLInputElement>) => Promise<any>;
13
+ }) {
14
+ const [isLoadingImport, setIsLoadingImport] = useState(false);
15
+ const [lastQueryParams, setLastQueryParams] = useState<TGetAllParams & TPathParams>();
16
+
17
+ const onImportChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
18
+ setIsLoadingImport(true);
19
+ await onImport?.(event)
20
+ .then((response) => {
21
+ console.log(response);
22
+ })
23
+ .finally(() => {
24
+ setIsLoadingImport(false);
25
+ });
26
+ }
27
+
28
+ const url = exportUrl + (lastQueryParams ? '?' + new URLSearchParams({
29
+ s: lastQueryParams.s,
30
+ sort: lastQueryParams.sort?.[0],
31
+ }).toString() : '');
32
+ const exportButton = <Tooltip title="Export">
33
+ <Link to={url} target={'_blank'}>
34
+ <Button icon={<DownloadOutlined />}/>
35
+ </Link>
36
+ </Tooltip>;
37
+
38
+ const importButton = <>
39
+ <Tooltip title="Import">
40
+ <label htmlFor="import-input">
41
+ <Button loading={isLoadingImport} icon={<UploadOutlined />} />
42
+ </label>
43
+ </Tooltip>
44
+ <input
45
+ type="file"
46
+ id="import-input"
47
+ style={{ display: "none" }}
48
+ accept=".xlsx, .xls"
49
+ onChange={onImportChange}
50
+ />
51
+ </>
52
+
53
+ return {
54
+ exportButton,
55
+ importButton,
56
+ setLastQueryParams
57
+ };
58
+ }
@@ -0,0 +1,18 @@
1
+ // Should be synced with common-backend/src/tools/ApiError.ts
2
+ type TApiErrorBodyType = {
3
+ statusCode: number
4
+ message: string
5
+ errors: {
6
+ field: string,
7
+ message: string
8
+ }[]
9
+ }
10
+
11
+ // Copied from api-client/generated/core/ApiError.ts
12
+ export class ApiError extends Error {
13
+ public readonly url: string;
14
+ public readonly status: number;
15
+ public readonly statusText: string;
16
+ public readonly body: TApiErrorBodyType;
17
+ public readonly request: any;
18
+ }
@@ -3,3 +3,4 @@ export * from './useFullscreen';
3
3
  export * from './numberTools';
4
4
  export * from './useTabs';
5
5
  export * from './WebsocketClient';
6
+ export * from './ApiError'