@centreon/ui 24.11.2 → 24.11.4

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 (75) hide show
  1. package/package.json +2 -3
  2. package/src/Dashboard/Dashboard.styles.ts +4 -3
  3. package/src/Dashboard/DashboardLayout.stories.tsx +1 -1
  4. package/src/Dashboard/Grid.tsx +17 -11
  5. package/src/Dashboard/Layout.tsx +56 -27
  6. package/src/FileDropZone/index.tsx +21 -23
  7. package/src/Form/CollapsibleGroup.tsx +3 -2
  8. package/src/Form/Form.cypress.spec.tsx +39 -0
  9. package/src/Form/Form.tsx +1 -0
  10. package/src/Form/Inputs/Autocomplete.tsx +27 -4
  11. package/src/Form/Inputs/ConnectedAutocomplete.tsx +20 -10
  12. package/src/Form/Inputs/File.tsx +69 -0
  13. package/src/Form/Inputs/Grid.tsx +30 -2
  14. package/src/Form/Inputs/Radio.tsx +12 -4
  15. package/src/Form/Inputs/Switch.tsx +10 -2
  16. package/src/Form/Inputs/Text.tsx +13 -4
  17. package/src/Form/Inputs/index.tsx +5 -2
  18. package/src/Form/Inputs/models.ts +18 -2
  19. package/src/Form/storiesData.tsx +15 -3
  20. package/src/Form/translatedLabels.ts +1 -0
  21. package/src/Graph/BarChart/BarChart.tsx +4 -1
  22. package/src/Graph/BarChart/ResponsiveBarChart.tsx +3 -2
  23. package/src/Graph/Chart/Chart.tsx +9 -2
  24. package/src/Graph/Chart/InteractiveComponents/AnchorPoint/useTickGraph.ts +2 -2
  25. package/src/Graph/Chart/InteractiveComponents/index.tsx +10 -2
  26. package/src/Graph/Chart/helpers/index.ts +5 -5
  27. package/src/Graph/Chart/index.tsx +7 -0
  28. package/src/Graph/Chart/models.ts +1 -0
  29. package/src/Graph/common/timeSeries/index.ts +15 -8
  30. package/src/InputField/Text/index.tsx +1 -1
  31. package/src/Listing/index.tsx +39 -27
  32. package/src/Listing/models.ts +8 -0
  33. package/src/MultiSelectEntries/index.tsx +0 -2
  34. package/src/PopoverMenu/index.tsx +9 -2
  35. package/src/SortableItems/index.tsx +1 -0
  36. package/src/ThemeProvider/index.tsx +1 -1
  37. package/src/ThemeProvider/palettes.ts +4 -4
  38. package/src/api/customFetch.ts +4 -1
  39. package/src/components/CrudPage/Actions/Actions.styles.ts +16 -0
  40. package/src/components/CrudPage/Actions/Actions.tsx +24 -0
  41. package/src/components/CrudPage/Actions/AddButton.tsx +23 -0
  42. package/src/components/CrudPage/Actions/Filters.tsx +25 -0
  43. package/src/components/CrudPage/Actions/Search.tsx +31 -0
  44. package/src/components/CrudPage/Actions/useSearch.tsx +24 -0
  45. package/src/components/CrudPage/Columns/Actions.tsx +88 -0
  46. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +559 -0
  47. package/src/components/CrudPage/CrudPage.stories.tsx +278 -0
  48. package/src/components/CrudPage/CrudPageRoot.tsx +142 -0
  49. package/src/components/CrudPage/DeleteModal.tsx +77 -0
  50. package/src/components/CrudPage/Form/AddModal.tsx +35 -0
  51. package/src/components/CrudPage/Form/Buttons.tsx +98 -0
  52. package/src/components/CrudPage/Form/UpdateModal.tsx +60 -0
  53. package/src/components/CrudPage/Listing.tsx +63 -0
  54. package/src/components/CrudPage/atoms.ts +30 -0
  55. package/src/components/CrudPage/hooks/useDeleteItem.ts +53 -0
  56. package/src/components/CrudPage/hooks/useGetItem.ts +36 -0
  57. package/src/components/CrudPage/hooks/useGetItems.ts +67 -0
  58. package/src/components/CrudPage/hooks/useListingQueryKey.ts +31 -0
  59. package/src/components/CrudPage/index.tsx +7 -0
  60. package/src/components/CrudPage/models.ts +118 -0
  61. package/src/components/CrudPage/utils.ts +4 -0
  62. package/src/components/DataTable/DataTable.cypress.spec.tsx +2 -1
  63. package/src/components/DataTable/DataTable.stories.tsx +17 -0
  64. package/src/components/DataTable/DataTable.styles.ts +1 -1
  65. package/src/components/DataTable/EmptyState/DataTableEmptyState.styles.ts +3 -1
  66. package/src/components/DataTable/EmptyState/DataTableEmptyState.tsx +6 -0
  67. package/src/components/DataTable/Item/DataTableItem.styles.ts +28 -2
  68. package/src/components/DataTable/Item/DataTableItem.tsx +19 -4
  69. package/src/components/Layout/AreaIndicator.tsx +1 -1
  70. package/src/components/Layout/PageLayout/PageLayout.styles.ts +7 -2
  71. package/src/components/Layout/PageLayout/PageLayoutBody.tsx +1 -0
  72. package/src/components/Modal/Modal.styles.ts +1 -1
  73. package/src/components/Zoom/Zoom.tsx +2 -2
  74. package/src/components/Zoom/ZoomContent.tsx +2 -2
  75. package/src/components/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.11.2",
3
+ "version": "24.11.4",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -18,6 +18,7 @@
18
18
  "cypress:run": "cypress run --component --browser=chrome",
19
19
  "tokens:transform": "TS_NODE_PROJECT=tsconfig.node.json ts-node style-dictionary.transform.ts"
20
20
  },
21
+ "type": "module",
21
22
  "sideEffects": false,
22
23
  "repository": {
23
24
  "type": "git",
@@ -53,8 +54,6 @@
53
54
  "@cypress/webpack-dev-server": "^3.10.1",
54
55
  "@faker-js/faker": "^8.4.1",
55
56
  "@mdx-js/react": "^3.0.1",
56
- "@modern-js/prod-server": "^2.58.1",
57
- "@modern-js/storybook": "^2.58.1",
58
57
  "@simonsmith/cypress-image-snapshot": "^9.1.0",
59
58
  "@storybook/addon-a11y": "^8.2.9",
60
59
  "@storybook/addon-docs": "^8.2.9",
@@ -40,7 +40,7 @@ export const useDashboardLayoutStyles = makeStyles<boolean>()(
40
40
  width: theme.spacing(1)
41
41
  },
42
42
  '& .react-resizable-handle.react-resizable-handle-s': {
43
- bottom: 0,
43
+ bottom: 4,
44
44
  cursor: 'ns-resize',
45
45
  height: theme.spacing(1),
46
46
  left: 0,
@@ -49,7 +49,7 @@ export const useDashboardLayoutStyles = makeStyles<boolean>()(
49
49
  width: `calc(100% - ${theme.spacing(3)})`
50
50
  },
51
51
  '& .react-resizable-handle.react-resizable-handle-se': {
52
- bottom: 0,
52
+ bottom: 4,
53
53
  cursor: 'nwse-resize',
54
54
  height: theme.spacing(2),
55
55
  right: 0,
@@ -62,7 +62,8 @@ export const useDashboardLayoutStyles = makeStyles<boolean>()(
62
62
  '& .react-resizable-handle:hover': {
63
63
  opacity: 1
64
64
  },
65
- position: 'relative'
65
+ position: 'relative',
66
+ height: '100%',
66
67
  }
67
68
  })
68
69
  );
@@ -95,7 +95,7 @@ export const normal = DashboardTemplate.bind({});
95
95
 
96
96
  export const withManyPanels = DashboardTemplate.bind({});
97
97
  withManyPanels.args = {
98
- layout: generateLayout(1000)
98
+ layout: generateLayout(100)
99
99
  };
100
100
 
101
101
  export const withItemHeader = DashboardTemplate.bind({});
@@ -1,4 +1,4 @@
1
- import { ReactElement, useMemo } from 'react';
1
+ import { MutableRefObject, ReactElement, useMemo } from 'react';
2
2
 
3
3
  import { scaleLinear } from '@visx/scale';
4
4
  import { Grid as VisxGrid } from '@visx/visx';
@@ -13,9 +13,15 @@ interface Props {
13
13
  columns: number;
14
14
  height: number;
15
15
  width: number;
16
+ containerRef: MutableRefObject<HTMLDivElement | null>;
16
17
  }
17
18
 
18
- const Grid = ({ width, height, columns }: Props): ReactElement => {
19
+ const Grid = ({
20
+ width,
21
+ height,
22
+ columns,
23
+ containerRef
24
+ }: Props): ReactElement => {
19
25
  const theme = useTheme();
20
26
 
21
27
  const xScale = useMemo(
@@ -44,19 +50,19 @@ const Grid = ({ width, height, columns }: Props): ReactElement => {
44
50
  .fill(0)
45
51
  .map((_, index) => index * tick);
46
52
 
53
+ const yTickValues = Array(numberOfRows)
54
+ .fill(0)
55
+ .map((_, index) => index);
56
+
47
57
  return useMemoComponent({
48
58
  Component: (
49
59
  <svg style={{ height, position: 'absolute', width }}>
50
- <VisxGrid.GridColumns
51
- height={height}
52
- scale={xScale}
53
- stroke={theme.palette.divider}
54
- tickValues={xTickValues}
55
- width={width}
56
- />
57
- <VisxGrid.GridRows
60
+ <VisxGrid.Grid
61
+ columnTickValues={xTickValues}
62
+ rowTickValues={yTickValues}
58
63
  height={height}
59
- scale={yScale}
64
+ yScale={yScale}
65
+ xScale={xScale}
60
66
  stroke={theme.palette.divider}
61
67
  top={-10}
62
68
  width={width}
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
3
  import { useSetAtom } from 'jotai';
4
4
  import GridLayout, { Layout, WidthProvider } from 'react-grid-layout';
@@ -6,10 +6,10 @@ import 'react-grid-layout/css/styles.css';
6
6
 
7
7
  import {
8
8
  ParentSize,
9
- Responsive as ResponsiveHeight,
10
9
  useMemoComponent
11
10
  } from '..';
12
11
 
12
+ import { Box } from '@mui/material';
13
13
  import { useDashboardLayoutStyles } from './Dashboard.styles';
14
14
  import Grid from './Grid';
15
15
  import { isResizingItemAtom } from './atoms';
@@ -26,6 +26,18 @@ interface DashboardLayoutProps<T> {
26
26
  layout: Array<T>;
27
27
  }
28
28
 
29
+ const bottom = (layout: Array<Layout>): number => {
30
+ let max = 0;
31
+ let bottomY = 0;
32
+
33
+ layout.forEach((panel) => {
34
+ bottomY = panel.y + panel.h;
35
+ if (bottomY > max) max = bottomY;
36
+ })
37
+
38
+ return max;
39
+ }
40
+
29
41
  const DashboardLayout = <T extends Layout>({
30
42
  children,
31
43
  changeLayout,
@@ -34,6 +46,8 @@ const DashboardLayout = <T extends Layout>({
34
46
  isStatic = false,
35
47
  additionalMemoProps = []
36
48
  }: DashboardLayoutProps<T>): JSX.Element => {
49
+ const dashboardContainerRef = useRef<HTMLDivElement | null>(null);
50
+
37
51
  const { classes } = useDashboardLayoutStyles(isStatic);
38
52
 
39
53
  const [columns, setColumns] = useState(getColumnsFromScreenSize());
@@ -52,6 +66,16 @@ const DashboardLayout = <T extends Layout>({
52
66
  setIsResizingItem(null);
53
67
  }, []);
54
68
 
69
+ const containerHeight = useMemo((): number | undefined => {
70
+ const nbRow = bottom(getLayout(layout));
71
+ const containerPaddingY = 4
72
+ return (
73
+ nbRow * rowHeight +
74
+ (nbRow - 1) * 20 +
75
+ containerPaddingY * 2
76
+ );
77
+ }, [layout, rowHeight])
78
+
55
79
  useEffect(() => {
56
80
  window.addEventListener('resize', resize);
57
81
 
@@ -62,31 +86,36 @@ const DashboardLayout = <T extends Layout>({
62
86
 
63
87
  return useMemoComponent({
64
88
  Component: (
65
- <ResponsiveHeight margin={40}>
66
- <ParentSize>
67
- {({ width, height }): JSX.Element => (
68
- <div className={classes.container}>
69
- {displayGrid && (
70
- <Grid columns={columns} height={height} width={width} />
71
- )}
72
- <ReactGridLayout
73
- cols={columns}
74
- containerPadding={[4, 0]}
75
- layout={getLayout(layout)}
76
- margin={[20, 20]}
77
- resizeHandles={['s', 'e', 'se']}
78
- rowHeight={rowHeight}
79
- width={width}
80
- onLayoutChange={changeLayout}
81
- onResizeStart={startResize}
82
- onResizeStop={stopResize}
83
- >
84
- {children}
85
- </ReactGridLayout>
86
- </div>
87
- )}
88
- </ParentSize>
89
- </ResponsiveHeight>
89
+ <Box ref={dashboardContainerRef} sx={{ overflowY: 'auto', overflowX: 'hidden' }}>
90
+ <ParentSize>
91
+ {({ width, height }): JSX.Element => (
92
+ <Box className={classes.container}>
93
+ {displayGrid && (
94
+ <Grid
95
+ columns={columns}
96
+ height={(containerHeight || 0) > height ? containerHeight : height}
97
+ width={width}
98
+ containerRef={dashboardContainerRef}
99
+ />
100
+ )}
101
+ <ReactGridLayout
102
+ cols={columns}
103
+ containerPadding={[4, 0]}
104
+ layout={getLayout(layout)}
105
+ margin={[20, 20]}
106
+ resizeHandles={['s', 'e', 'se']}
107
+ rowHeight={rowHeight}
108
+ width={width}
109
+ onLayoutChange={changeLayout}
110
+ onResizeStart={startResize}
111
+ onResizeStop={stopResize}
112
+ >
113
+ {children}
114
+ </ReactGridLayout>
115
+ </Box>
116
+ )}
117
+ </ParentSize>
118
+ </Box>
90
119
  ),
91
120
  memoProps: [columns, layout, displayGrid, isStatic, ...additionalMemoProps]
92
121
  });
@@ -30,23 +30,15 @@ interface StylesProps {
30
30
  const useStyles = makeStyles<StylesProps>()(
31
31
  (theme, { hasCustomDropZoneContent, isDraggingOver }) => ({
32
32
  dropzone: {
33
- '&:hover': hasCustomDropZoneContent
34
- ? undefined
35
- : {
36
- backgroundColor: alpha(theme.palette.primary.main, 0.1),
37
- border: `${theme.spacing(0.3)} dashed ${
38
- theme.palette.primary.main
39
- }`,
40
- boxShadow: theme.shadows[3],
41
- cursor: 'pointer'
42
- },
43
- border: `${theme.spacing(0.3)} dashed ${
44
- hasCustomDropZoneContent && !isDraggingOver
45
- ? 'transparent'
46
- : theme.palette.primary.main
47
- }`,
33
+ '&:hover': {
34
+ backgroundColor: alpha(theme.palette.primary.main, 0.1),
35
+ boxShadow: theme.shadows[3],
36
+ cursor: 'pointer'
37
+ },
38
+ border: `${theme.spacing(0.3)} dashed ${theme.palette.primary.main}`,
48
39
  boxShadow: isDraggingOver ? theme.shadows[3] : theme.shadows[0],
49
- padding: hasCustomDropZoneContent ? undefined : theme.spacing(1),
40
+ borderRadius: `${theme.shape.borderRadius}px`,
41
+ padding: theme.spacing(0.5, 1),
50
42
  width: hasCustomDropZoneContent ? '100%' : theme.spacing(50)
51
43
  },
52
44
  dropzoneInfo: {
@@ -66,7 +58,7 @@ const useStyles = makeStyles<StylesProps>()(
66
58
  export type CustomDropZoneContentProps = Pick<
67
59
  UseDropzoneState,
68
60
  'openFileExplorer'
69
- >;
61
+ > & { files: FileList | null; label?: string };
70
62
 
71
63
  interface Props {
72
64
  CustomDropZoneContent?: ({
@@ -79,6 +71,7 @@ interface Props {
79
71
  maxFileSize?: number;
80
72
  multiple?: boolean;
81
73
  resetFilesStatusAndUploadData: () => void;
74
+ label?: string;
82
75
  }
83
76
 
84
77
  const getExtensions = cond([
@@ -115,7 +108,8 @@ const Dropzone = ({
115
108
  accept,
116
109
  CustomDropZoneContent,
117
110
  maxFileSize,
118
- className
111
+ className,
112
+ label
119
113
  }: Props): JSX.Element => {
120
114
  const hasCustomDropZoneContent = !isNil(CustomDropZoneContent);
121
115
  const {
@@ -145,25 +139,29 @@ const Dropzone = ({
145
139
  <div>
146
140
  <Box
147
141
  className={cx(classes.dropzone, className)}
148
- onClick={!hasCustomDropZoneContent ? openFileExplorer : undefined}
142
+ onClick={openFileExplorer}
149
143
  onDragLeave={dragOver(false)}
150
144
  onDragOver={dragOver(true)}
151
145
  onDrop={dropFiles}
152
146
  >
153
147
  <div className={classes.dropzoneInfo}>
154
148
  {hasCustomDropZoneContent ? (
155
- <CustomDropZoneContent openFileExplorer={openFileExplorer} />
149
+ <CustomDropZoneContent
150
+ openFileExplorer={openFileExplorer}
151
+ files={files}
152
+ label={label}
153
+ />
156
154
  ) : (
157
- <>
155
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
158
156
  <PostAddIcon color="primary" fontSize="large" />
159
157
  <Typography>
160
158
  {t(labelDropOr)} {t(labelSelectAFile)}
161
159
  </Typography>
162
- </>
160
+ </Box>
163
161
  )}
164
162
  <input
165
163
  accept={accept}
166
- aria-label={t(labelSelectAFile) || ''}
164
+ aria-label={t(labelSelectAFile)}
167
165
  className={classes.input}
168
166
  multiple={multiple}
169
167
  ref={fileInputRef}
@@ -43,7 +43,8 @@ const useStyles = makeStyles()((theme) => ({
43
43
  },
44
44
  tooltip: {
45
45
  maxWidth: theme.spacing(60)
46
- }
46
+ },
47
+ title: {}
47
48
  }));
48
49
 
49
50
  const CollapsibleGroup = ({
@@ -97,7 +98,7 @@ const CollapsibleGroup = ({
97
98
  <div className={classes.groupTitleIcon}>
98
99
  <Typography
99
100
  className="groupText"
100
- variant="h5"
101
+ variant="h6"
101
102
  {...group?.titleAttributes}
102
103
  >
103
104
  {t(group?.name as string)}
@@ -131,3 +131,42 @@ describe('Form list', () => {
131
131
  cy.makeSnapshot();
132
132
  });
133
133
  });
134
+
135
+ const initializeFile = (): void => {
136
+ cy.mount({
137
+ Component: (
138
+ <Form
139
+ initialValues={{
140
+ list: []
141
+ }}
142
+ inputs={[
143
+ {
144
+ fieldName: 'file',
145
+ group: '',
146
+ label: 'json',
147
+ type: InputType.File,
148
+ file: {
149
+ accept: '.json'
150
+ }
151
+ }
152
+ ]}
153
+ submit={cy.stub()}
154
+ validationSchema={object()}
155
+ />
156
+ )
157
+ });
158
+ };
159
+
160
+ describe('File', () => {
161
+ it('uploads a file when a file is selected', () => {
162
+ initializeFile();
163
+
164
+ cy.contains('Drop or select a file').should('be.visible');
165
+ cy.findByLabelText('select a file').selectFile('package.json', {
166
+ force: true
167
+ });
168
+ cy.contains('package.json').should('be.visible');
169
+
170
+ cy.makeSnapshot();
171
+ });
172
+ });
package/src/Form/Form.tsx CHANGED
@@ -68,6 +68,7 @@ const Form = <T extends object>({
68
68
 
69
69
  return (
70
70
  <Formik<T>
71
+ enableReinitialize
71
72
  initialValues={initialValues}
72
73
  validate={validate}
73
74
  validationSchema={validationSchema}
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
2
 
3
3
  import { FormikValues, useFormikContext } from 'formik';
4
- import { path, equals, isNil, map, not, prop, type } from 'ramda';
4
+ import { equals, isNil, map, not, path, prop, type } from 'ramda';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { FormHelperText, Stack } from '@mui/material';
@@ -53,7 +53,15 @@ const Autocomplete = ({
53
53
 
54
54
  const [inputText, setInputText] = useState('');
55
55
 
56
- const { values, setFieldValue, errors } = useFormikContext<FormikValues>();
56
+ const {
57
+ values,
58
+ setFieldValue,
59
+ setFieldTouched,
60
+ errors,
61
+ touched,
62
+ setValues,
63
+ setTouched
64
+ } = useFormikContext<FormikValues>();
57
65
 
58
66
  const isMultiple = equals(inputType, InputType.MultiAutocomplete);
59
67
 
@@ -67,11 +75,20 @@ const Autocomplete = ({
67
75
  setInputText('');
68
76
 
69
77
  if (change) {
70
- change({ setFieldValue, value: normalizedNewValues });
78
+ setFieldTouched(fieldName, true, false);
79
+ change({
80
+ setFieldValue,
81
+ value: normalizedNewValues,
82
+ setFieldTouched,
83
+ setValues,
84
+ values,
85
+ setTouched
86
+ });
71
87
 
72
88
  return;
73
89
  }
74
90
 
91
+ setFieldTouched(fieldName, true, false);
75
92
  setFieldValue(fieldName, normalizedNewValues);
76
93
  };
77
94
 
@@ -83,6 +100,10 @@ const Autocomplete = ({
83
100
  );
84
101
 
85
102
  const getError = useCallback((): Array<string> | undefined => {
103
+ if (!path([...fieldName.split('.')], touched)) {
104
+ return undefined;
105
+ }
106
+
86
107
  const error = path([...fieldName.split('.')], errors) as
87
108
  | Array<string>
88
109
  | string
@@ -111,7 +132,7 @@ const Autocomplete = ({
111
132
  const filteredError = formattedError?.filter(Boolean);
112
133
 
113
134
  return (filteredError as Array<string>) || undefined;
114
- }, [errors, fieldName, isMultiple, selectedValues]);
135
+ }, [errors, fieldName, isMultiple, selectedValues, touched]);
115
136
 
116
137
  const textChange = useCallback(
117
138
  (event): void => setInputText(event.target.value),
@@ -167,6 +188,7 @@ const Autocomplete = ({
167
188
  value={getValues() ?? null}
168
189
  onChange={changeValues}
169
190
  onTextChange={textChange}
191
+ style={{ width: autocomplete?.fullWidth ?? true ? 'auto' : '180px' }}
170
192
  />
171
193
  {inputErrors && (
172
194
  <Stack>
@@ -180,6 +202,7 @@ const Autocomplete = ({
180
202
  </div>
181
203
  ),
182
204
  memoProps: [
205
+ values,
183
206
  getValues(),
184
207
  inputErrors,
185
208
  additionalLabel,
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
 
3
3
  import { FormikValues, useFormikContext } from 'formik';
4
- import { path, equals, isEmpty, propEq, reject, split } from 'ramda';
4
+ import { equals, isEmpty, path, propEq, reject, split } from 'ramda';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import {
@@ -30,8 +30,15 @@ const ConnectedAutocomplete = ({
30
30
  }: InputPropsWithoutGroup): JSX.Element => {
31
31
  const { t } = useTranslation();
32
32
 
33
- const { values, touched, errors, setFieldValue, setFieldTouched } =
34
- useFormikContext<FormikValues>();
33
+ const {
34
+ values,
35
+ touched,
36
+ errors,
37
+ setFieldValue,
38
+ setFieldTouched,
39
+ setValues,
40
+ setTouched
41
+ } = useFormikContext<FormikValues>();
35
42
 
36
43
  const filterKey = connectedAutocomplete?.filterKey || defaultFilterKey;
37
44
 
@@ -58,18 +65,20 @@ const ConnectedAutocomplete = ({
58
65
  const changeAutocomplete = useCallback(
59
66
  (_, value): void => {
60
67
  if (change) {
61
- change({ setFieldValue, value });
68
+ change({
69
+ setFieldValue,
70
+ value,
71
+ setFieldTouched,
72
+ setValues,
73
+ values,
74
+ setTouched
75
+ });
62
76
 
63
77
  return;
64
78
  }
65
79
 
80
+ setFieldTouched(fieldName, true, false);
66
81
  setFieldValue(fieldName, value);
67
-
68
- if (path(fieldNamePath, touched)) {
69
- return;
70
- }
71
-
72
- setFieldTouched(fieldName, true);
73
82
  },
74
83
  [fieldName, touched, additionalMemoProps]
75
84
  );
@@ -105,6 +114,7 @@ const ConnectedAutocomplete = ({
105
114
  const deleteItem = (_, option): void => {
106
115
  const newValue = reject(propEq(option.id, 'id'), value);
107
116
 
117
+ setFieldTouched(fieldName, true, false);
108
118
  setFieldValue(fieldName, newValue);
109
119
  };
110
120
 
@@ -0,0 +1,69 @@
1
+ import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
2
+ import { Box, Typography } from '@mui/material';
3
+ import { FormikValues, useFormikContext } from 'formik';
4
+ import { path, split } from 'ramda';
5
+ import { useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import FileDropZone, { transformFileListToArray } from '../../FileDropZone';
8
+ import { InputPropsWithoutGroup } from './models';
9
+
10
+ const File = ({
11
+ fieldName,
12
+ file,
13
+ change,
14
+ dataTestId,
15
+ label
16
+ }: InputPropsWithoutGroup): JSX.Element => {
17
+ const { t } = useTranslation();
18
+
19
+ const { values, setFieldValue, setFieldTouched } =
20
+ useFormikContext<FormikValues>();
21
+
22
+ const fieldNamePath = split('.', fieldName);
23
+
24
+ const files = useMemo(
25
+ () => path(fieldNamePath, values),
26
+ [values]
27
+ ) as FileList;
28
+
29
+ const filesArray = transformFileListToArray(files);
30
+
31
+ const changeFiles = (newFiles: FileList | null): void => {
32
+ if (change) {
33
+ change({ setFieldValue, setFieldTouched, value: newFiles });
34
+
35
+ return;
36
+ }
37
+
38
+ setFieldValue(fieldName, newFiles);
39
+ };
40
+
41
+ return (
42
+ <Box data-testid={dataTestId} aria-label={t(label)}>
43
+ <Typography variant="h6">{t(label)}</Typography>
44
+ <Box sx={{ display: 'flex', gap: 1, flexDirection: 'column' }}>
45
+ <FileDropZone
46
+ {...file}
47
+ accept={file?.accept || '*'}
48
+ files={files || null}
49
+ changeFiles={changeFiles}
50
+ resetFilesStatusAndUploadData={() => undefined}
51
+ label={label}
52
+ />
53
+ <Box sx={{ display: 'flex', gap: 1, flexDirection: 'column' }}>
54
+ {filesArray.map((file) => (
55
+ <Box
56
+ key={file.name}
57
+ sx={{ display: 'flex', gap: 1, flexDirection: 'row' }}
58
+ >
59
+ <DescriptionOutlinedIcon color="success" fontSize="small" />
60
+ <Typography>{file.name}</Typography>
61
+ </Box>
62
+ ))}
63
+ </Box>
64
+ </Box>
65
+ </Box>
66
+ );
67
+ };
68
+
69
+ export default File;
@@ -2,6 +2,8 @@ import { makeStyles } from 'tss-react/mui';
2
2
 
3
3
  import { InputPropsWithoutGroup } from './models';
4
4
 
5
+ import { Box, Typography } from '@mui/material';
6
+ import { FormikValues, useFormikContext } from 'formik';
5
7
  import { getInput } from '.';
6
8
 
7
9
  interface StylesProps {
@@ -22,13 +24,22 @@ const useStyles = makeStyles<StylesProps>()(
22
24
  })
23
25
  );
24
26
 
25
- const Grid = ({ grid }: InputPropsWithoutGroup): JSX.Element => {
27
+ const Grid = ({
28
+ grid,
29
+ hideInput
30
+ }: InputPropsWithoutGroup): JSX.Element | null => {
26
31
  const { classes, cx } = useStyles({
27
32
  alignItems: grid?.alignItems,
28
33
  columns: grid?.columns.length,
29
34
  gridTemplateColumns: grid?.gridTemplateColumns
30
35
  });
31
36
 
37
+ const { values } = useFormikContext<FormikValues>();
38
+
39
+ if (hideInput?.(values) ?? false) {
40
+ return null;
41
+ }
42
+
32
43
  const className = grid?.className || '';
33
44
 
34
45
  return (
@@ -36,7 +47,24 @@ const Grid = ({ grid }: InputPropsWithoutGroup): JSX.Element => {
36
47
  {grid?.columns.map((field) => {
37
48
  const Input = getInput(field.type);
38
49
 
39
- return <Input key={field.fieldName} {...field} />;
50
+ if (field.hideInput?.(values) ?? false) {
51
+ return null;
52
+ }
53
+
54
+ return (
55
+ <Box sx={{ width: '100%' }} key={field.fieldName}>
56
+ {field.additionalLabel && (
57
+ <Typography
58
+ sx={{ marginBottom: 0.5, color: 'primary.main' }}
59
+ className={cx(field?.additionalLabelClassName)}
60
+ variant="h6"
61
+ >
62
+ {field.additionalLabel}
63
+ </Typography>
64
+ )}
65
+ <Input {...field} />
66
+ </Box>
67
+ );
40
68
  })}
41
69
  </div>
42
70
  );