@centreon/ui 24.4.44 → 24.4.45

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 (138) hide show
  1. package/package.json +23 -14
  2. package/public/mockServiceWorker.js +1 -1
  3. package/src/Button/Icon/index.tsx +1 -1
  4. package/src/Button/Save/StartIcon.tsx +3 -3
  5. package/src/Button/Save/index.tsx +9 -5
  6. package/src/Checkbox/Checkbox.tsx +2 -2
  7. package/src/Checkbox/CheckboxGroup/index.tsx +2 -2
  8. package/src/Dashboard/Item.tsx +1 -1
  9. package/src/Dashboard/Layout.tsx +2 -2
  10. package/src/Dialog/index.tsx +1 -1
  11. package/src/FallbackPage/FallbackPage.tsx +3 -3
  12. package/src/FileDropZone/index.tsx +3 -1
  13. package/src/Form/Form.cypress.spec.tsx +133 -0
  14. package/src/Form/Inputs/CheckboxGroup.tsx +1 -4
  15. package/src/Form/Inputs/List/Content.tsx +62 -0
  16. package/src/Form/Inputs/List/List.styles.ts +29 -0
  17. package/src/Form/Inputs/List/List.tsx +58 -0
  18. package/src/Form/Inputs/List/useList.ts +81 -0
  19. package/src/Form/Inputs/index.tsx +3 -1
  20. package/src/Form/Inputs/models.ts +9 -1
  21. package/src/Graph/BarStack/BarStack.cypress.spec.tsx +154 -0
  22. package/src/Graph/BarStack/BarStack.stories.tsx +123 -0
  23. package/src/Graph/BarStack/BarStack.styles.ts +36 -0
  24. package/src/Graph/BarStack/BarStack.tsx +14 -0
  25. package/src/Graph/BarStack/ResponsiveBarStack.tsx +208 -0
  26. package/src/Graph/BarStack/index.ts +1 -0
  27. package/src/Graph/BarStack/models.ts +19 -0
  28. package/src/Graph/BarStack/useResponsiveBarStack.ts +139 -0
  29. package/src/Graph/Gauge/Gauge.cypress.spec.tsx +102 -0
  30. package/src/Graph/Gauge/Gauge.tsx +1 -1
  31. package/src/Graph/HeatMap/HeatMap.cypress.spec.tsx +145 -0
  32. package/src/Graph/HeatMap/HeatMap.stories.tsx +0 -25
  33. package/src/Graph/HeatMap/ResponsiveHeatMap.tsx +8 -2
  34. package/src/Graph/Legend/Legend.tsx +21 -0
  35. package/src/Graph/Legend/index.ts +1 -0
  36. package/src/Graph/Legend/models.ts +11 -0
  37. package/src/Graph/LineChart/BasicComponents/Lines/Threshold/Circle.tsx +2 -2
  38. package/src/Graph/LineChart/BasicComponents/Lines/Threshold/index.tsx +5 -4
  39. package/src/Graph/LineChart/BasicComponents/Thresholds.tsx +2 -2
  40. package/src/Graph/LineChart/BasicComponents/useFilterLines.ts +1 -1
  41. package/src/Graph/LineChart/InteractiveComponents/AnchorPoint/GuidingLines.tsx +2 -2
  42. package/src/Graph/LineChart/InteractiveComponents/Annotations/Annotation/index.tsx +2 -3
  43. package/src/Graph/LineChart/InteractiveComponents/Annotations/EventAnnotations.tsx +1 -1
  44. package/src/Graph/LineChart/Legend/Legend.styles.ts +1 -1
  45. package/src/Graph/LineChart/Legend/LegendHeader.tsx +1 -1
  46. package/src/Graph/LineChart/Legend/useInteractiveValues.ts +2 -2
  47. package/src/Graph/LineChart/Legend/useLegend.ts +3 -3
  48. package/src/Graph/LineChart/helpers/doc.ts +16 -13
  49. package/src/Graph/LineChart/helpers/index.ts +1 -1
  50. package/src/Graph/LineChart/index.stories.tsx +4 -2
  51. package/src/Graph/LineChart/index.tsx +1 -1
  52. package/src/Graph/PieChart/PieChart.cypress.spec.tsx +169 -0
  53. package/src/Graph/PieChart/PieChart.stories.tsx +194 -0
  54. package/src/Graph/PieChart/PieChart.styles.ts +39 -0
  55. package/src/Graph/PieChart/PieChart.tsx +14 -0
  56. package/src/Graph/PieChart/ResponsivePie.tsx +251 -0
  57. package/src/Graph/PieChart/index.ts +1 -0
  58. package/src/Graph/PieChart/models.ts +19 -0
  59. package/src/Graph/PieChart/useResponsivePie.ts +86 -0
  60. package/src/Graph/SingleBar/SingleBar.cypress.spec.tsx +121 -0
  61. package/src/Graph/SingleBar/Thresholds.tsx +2 -2
  62. package/src/Graph/Text/Text.cypress.spec.tsx +101 -0
  63. package/src/Graph/Text/Text.stories.tsx +60 -4
  64. package/src/Graph/Text/Text.tsx +1 -1
  65. package/src/Graph/common/testUtils.ts +71 -0
  66. package/src/Graph/common/timeSeries/index.ts +22 -14
  67. package/src/Graph/common/utils.ts +19 -0
  68. package/src/Graph/index.ts +3 -0
  69. package/src/Graph/translatedLabels.ts +1 -0
  70. package/src/InputField/Select/Autocomplete/Connected/index.tsx +10 -7
  71. package/src/InputField/Select/Autocomplete/Draggable/SortableList.tsx +1 -1
  72. package/src/InputField/Select/Autocomplete/Draggable/SortableListContent.tsx +1 -1
  73. package/src/InputField/Select/Autocomplete/Draggable/index.tsx +1 -1
  74. package/src/InputField/Select/Autocomplete/index.tsx +121 -115
  75. package/src/InputField/Select/IconPopover/index.tsx +2 -2
  76. package/src/InputField/Select/index.tsx +1 -1
  77. package/src/InputField/Text/index.tsx +2 -2
  78. package/src/Listing/ActionBar/index.tsx +9 -8
  79. package/src/Listing/Cell/DataCell.styles.ts +3 -0
  80. package/src/Listing/Cell/DataCell.tsx +23 -5
  81. package/src/Listing/Header/ListingHeader.tsx +1 -1
  82. package/src/Listing/Listing.cypress.spec.tsx +80 -4
  83. package/src/Listing/Listing.styles.ts +4 -7
  84. package/src/Listing/index.stories.tsx +37 -3
  85. package/src/Listing/index.test.tsx +1 -1
  86. package/src/Listing/index.tsx +4 -3
  87. package/src/Listing/models.ts +1 -0
  88. package/src/Module/Module.cypress.spec.tsx +129 -0
  89. package/src/Module/index.tsx +2 -4
  90. package/src/RichTextEditor/RichTextEditor.tsx +12 -1
  91. package/src/SortableItems/index.tsx +2 -7
  92. package/src/ThemeProvider/index.tsx +24 -0
  93. package/src/TimePeriods/CustomTimePeriod/CompactCustomTimePeriod.styles.ts +6 -7
  94. package/src/TimePeriods/CustomTimePeriod/PopoverCustomTimePeriod/PickersStartEndDate.tsx +8 -3
  95. package/src/TimePeriods/CustomTimePeriod/PopoverCustomTimePeriod/models.ts +0 -2
  96. package/src/TimePeriods/DateTimePickerInput.tsx +56 -19
  97. package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +12 -9
  98. package/src/TimePeriods/TimePeriods.cypress.spec.tsx +9 -33
  99. package/src/TimePeriods/helpers/index.ts +1 -1
  100. package/src/TimePeriods/index.stories.tsx +12 -4
  101. package/src/TimePeriods/index.tsx +2 -2
  102. package/src/api/QueryProvider.tsx +1 -1
  103. package/src/api/TestQueryProvider.tsx +1 -1
  104. package/src/api/useFetchQuery/index.ts +27 -23
  105. package/src/api/useMutationQuery/index.test.ts +4 -4
  106. package/src/api/useMutationQuery/index.ts +60 -25
  107. package/src/components/Button/Icon/IconButton.tsx +6 -2
  108. package/src/components/DataTable/DataListing.tsx +6 -0
  109. package/src/components/DataTable/DataTable.cypress.spec.tsx +193 -0
  110. package/src/components/DataTable/DataTable.stories.tsx +40 -0
  111. package/src/components/DataTable/DataTable.styles.ts +3 -0
  112. package/src/components/DataTable/DataTable.tsx +3 -3
  113. package/src/components/DataTable/Item/DataTableItem.styles.ts +7 -2
  114. package/src/components/DataTable/Item/DataTableItem.tsx +4 -4
  115. package/src/components/DataTable/index.ts +3 -1
  116. package/src/components/Form/AccessRights/ShareInput/ContactSwitch.tsx +3 -3
  117. package/src/components/Form/AccessRights/ShareInput/ShareInput.tsx +1 -0
  118. package/src/components/Form/Dashboard/DashboardForm.tsx +15 -12
  119. package/src/components/Layout/PageLayout/PageLayout.tsx +1 -1
  120. package/src/components/Layout/PageLayout/PageLayoutActions.tsx +1 -0
  121. package/src/components/Layout/PageLayout/PageLayoutBody.tsx +1 -0
  122. package/src/components/Layout/PageLayout/PageLayoutHeader.tsx +5 -1
  123. package/src/components/Layout/PageLayout/PageQuickAccess.tsx +76 -0
  124. package/src/components/Layout/PageLayout/index.ts +3 -1
  125. package/src/components/Layout/PageLayout.cypress.spec.tsx +66 -0
  126. package/src/components/Modal/Modal.styles.ts +1 -1
  127. package/src/components/Tooltip/ConfirmationTooltip/ConfirmationTooltip.stories.tsx +3 -3
  128. package/src/components/Tooltip/ConfirmationTooltip/ConfirmationTooltip.tsx +1 -1
  129. package/src/components/Tooltip/ConfirmationTooltip/models.ts +1 -1
  130. package/src/index.ts +2 -2
  131. package/src/queryParameters/url/index.ts +5 -1
  132. package/src/utils/index.ts +2 -1
  133. package/src/utils/{useLicenseExpirationWarning.cypress.spec.tsx → useLicenseExpirationWarning.test.tsx} +48 -37
  134. package/src/utils/useLicenseExpirationWarning.ts +18 -18
  135. package/src/utils/usePluralizedTranslation.ts +21 -0
  136. package/src/screens/dashboard/DashboardsDetail.stories.tsx +0 -108
  137. package/src/screens/dashboard/DashboardsOverview.stories.tsx +0 -281
  138. package/src/utils/useDateTimePickerAdapter.ts +0 -309
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.4.44",
3
+ "version": "24.4.45",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -12,6 +12,7 @@
12
12
  "test:ci": "jest --silent --reporter=jest-junit",
13
13
  "cypress:ui": "cypress open --component --browser=chrome",
14
14
  "cypress:cli:updateSnapshot": "pnpm cypress:cli --env updateSnapshots=true",
15
+ "cypress:run:coverage": "cypress run --component --browser=chrome --env codeCoverageTasksRegistered=true",
15
16
  "cypress:cli": "cypress run --component --browser=chrome",
16
17
  "tokens:transform": "TS_NODE_PROJECT=tsconfig.node.json ts-node style-dictionary.transform.ts"
17
18
  },
@@ -27,6 +28,7 @@
27
28
  "author": {
28
29
  "name": "centreon@centreon.com"
29
30
  },
31
+ "baseCodeCoveragePercentage": 60,
30
32
  "license": "GPL-2.0",
31
33
  "bugs": {
32
34
  "url": "https://github.com/centreon/centreon/issues"
@@ -73,16 +75,19 @@
73
75
  "@testing-library/react-hooks": "^8.0.1",
74
76
  "@types/cypress": "^1.1.3",
75
77
  "@types/jest": "^27.5.0",
76
- "@types/ramda": "^0.28.12",
78
+ "@types/mocha": "^10.0.6",
79
+ "@types/ramda": "^0.29.6",
77
80
  "@types/react": "^18.0.26",
78
81
  "@types/testing-library__jest-dom": "^5.14.6",
79
82
  "@vitejs/plugin-react": "^4.0.0",
80
83
  "@vitejs/plugin-react-swc": "^3.3.0",
81
84
  "axios-mock-adapter": "^1.21.4",
82
- "cypress": "^12.8.0",
85
+ "cypress": "^13.3.1",
83
86
  "identity-obj-proxy": "^3.0.0",
84
87
  "jest-transform-stub": "^2.0.0",
85
88
  "mochawesome": "^7.1.3",
89
+ "msw": "1.3.2",
90
+ "msw-storybook-addon": "^1.10.0",
86
91
  "puppeteer": "^13.7.0",
87
92
  "react": "^18.2.0",
88
93
  "react-dom": "^18.2.0",
@@ -97,31 +102,35 @@
97
102
  "ts-node": "^10.9.1",
98
103
  "use-resize-observer": "^9.1.0",
99
104
  "vite": "^4.3.5",
105
+ "vite-plugin-istanbul": "^5.0.0",
100
106
  "vite-plugin-svgr": "^3.2.0",
101
107
  "vite-plugin-turbosnap": "^1.0.2"
102
108
  },
103
109
  "dependencies": {
104
- "@lexical/link": "0.11.3",
105
- "@lexical/list": "^0.11.3",
106
- "@lexical/react": "0.11.3",
107
- "@lexical/rich-text": "^0.11.3",
108
- "@lexical/selection": "0.11.3",
109
- "@lexical/utils": "0.11.3",
110
+ "@lexical/html": "^0.12.2",
111
+ "@lexical/link": "^0.12.2",
112
+ "@lexical/list": "^0.12.2",
113
+ "@lexical/react": "^0.12.2",
114
+ "@lexical/rich-text": "^0.12.2",
115
+ "@lexical/selection": "^0.12.2",
116
+ "@lexical/utils": "^0.12.2",
110
117
  "@react-spring/web": "^9.7.3",
111
118
  "@visx/curve": "^2.1.0",
112
119
  "@visx/group": "^3.3.0",
120
+ "@visx/legend": "^3.5.0",
113
121
  "@visx/pattern": "^3.0.0",
114
122
  "@visx/scale": "^3.0.0",
115
123
  "@visx/shape": "^2.12.2",
124
+ "@visx/text": "^3.3.0",
116
125
  "@visx/threshold": "^2.12.2",
117
126
  "@visx/visx": "2.16.0",
118
127
  "anylogger": "^1.0.11",
119
128
  "d3-array": "3.2.0",
120
129
  "humanize-duration": "^3.27.3",
121
- "lexical": "0.11.3",
122
- "msw": "0.49.1",
123
- "msw-storybook-addon": "^1.10.0",
124
- "notistack": "^2.0.8",
130
+ "lexical": "^0.12.2",
131
+ "notistack": "^3.0.1",
132
+ "numeral": "^2.0.6",
133
+ "ramda": "0.29.1",
125
134
  "react-grid-layout": "^1.3.4",
126
135
  "react-html-parser": "^2.0.2",
127
136
  "react-resizable": "^3.0.5",
@@ -137,7 +146,7 @@
137
146
  "@mui/icons-material": "5.x",
138
147
  "@mui/material": "5.x",
139
148
  "@mui/styles": "5.x",
140
- "@tanstack/react-query": "4.x",
149
+ "@tanstack/react-query": "5.x",
141
150
  "axios": "0.x",
142
151
  "dayjs": "1.x",
143
152
  "formik": "2.x",
@@ -2,7 +2,7 @@
2
2
  /* tslint:disable */
3
3
 
4
4
  /**
5
- * Mock Service Worker (0.49.1).
5
+ * Mock Service Worker (1.3.2).
6
6
  * @see https://github.com/mswjs/msw
7
7
  * - Please do NOT modify this file.
8
8
  * - Please do NOT serve this file on production.
@@ -21,7 +21,7 @@ type Props = {
21
21
  ariaLabel?: string;
22
22
  className?: string;
23
23
  onClick: (event) => void;
24
- title?: string;
24
+ title?: string | JSX.Element;
25
25
  tooltipClassName?: string;
26
26
  tooltipPlacement?:
27
27
  | 'bottom'
@@ -15,9 +15,9 @@ interface Props {
15
15
 
16
16
  const StartIcon = ({ startIconConfig }: Props): JSX.Element | null =>
17
17
  cond<Array<StartIconConfigProps>, JSX.Element | null>([
18
- [pipe(propEq('hasLabel', true), not), always(null)],
19
- [propEq('succeeded', true), always(<CheckIcon />)],
20
- [propEq('loading', true), always(<SaveIcon />)],
18
+ [pipe(propEq(true, 'hasLabel'), not), always(null)],
19
+ [propEq(true, 'succeeded'), always(<CheckIcon />)],
20
+ [propEq(true, 'loading'), always(<SaveIcon />)],
21
21
  [T, always(<SaveIcon />)]
22
22
  ])(startIconConfig);
23
23
 
@@ -1,13 +1,13 @@
1
1
  import { any, isEmpty, isNil, not, or, pipe } from 'ramda';
2
2
  import { makeStyles } from 'tss-react/mui';
3
3
 
4
+ import { LoadingButton, LoadingButtonProps } from '@mui/lab';
4
5
  import { Theme, Tooltip } from '@mui/material';
5
- import { LoadingButton } from '@mui/lab';
6
6
 
7
7
  import { getNormalizedId } from '../../utils';
8
8
 
9
- import StartIcon from './StartIcon';
10
9
  import Content from './Content';
10
+ import StartIcon from './StartIcon';
11
11
 
12
12
  const useStyles = makeStyles()((theme: Theme) => ({
13
13
  loadingButton: {
@@ -15,13 +15,14 @@ const useStyles = makeStyles()((theme: Theme) => ({
15
15
  }
16
16
  }));
17
17
 
18
- interface Props extends Record<string, unknown> {
18
+ interface Props {
19
19
  className?: string;
20
20
  labelLoading?: string;
21
21
  labelSave?: string;
22
22
  labelSucceeded?: string;
23
23
  loading?: boolean;
24
24
  size?: 'small' | 'medium' | 'large';
25
+ startIcon?: boolean;
25
26
  succeeded?: boolean;
26
27
  tooltip?: string;
27
28
  tooltipLabel?: string;
@@ -45,8 +46,9 @@ const SaveButton = ({
45
46
  labelSave = '',
46
47
  size = 'small',
47
48
  className,
49
+ startIcon = true,
48
50
  ...rest
49
- }: Props): JSX.Element => {
51
+ }: Props & LoadingButtonProps): JSX.Element => {
50
52
  const { classes, cx } = useStyles();
51
53
  const hasLabel = hasValue([labelLoading, labelSave, labelSucceeded]);
52
54
 
@@ -73,7 +75,9 @@ const SaveButton = ({
73
75
  loading={loading}
74
76
  loadingPosition={labelLoading ? 'start' : 'center'}
75
77
  size={size}
76
- startIcon={<StartIcon startIconConfig={startIconConfig} />}
78
+ startIcon={
79
+ startIcon && <StartIcon startIconConfig={startIconConfig} />
80
+ }
77
81
  variant="contained"
78
82
  {...rest}
79
83
  >
@@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';
2
2
  import { T, always, cond, equals } from 'ramda';
3
3
 
4
4
  import { SvgIconComponent } from '@mui/icons-material';
5
- import Typography, { TypographyTypeMap } from '@mui/material/Typography';
5
+ import Typography, { TypographyProps } from '@mui/material/Typography';
6
6
  import { FormControlLabel, Checkbox as MuiCheckbox, Box } from '@mui/material';
7
7
 
8
8
  export type LabelPlacement = 'bottom' | 'top' | 'end' | 'start' | undefined;
@@ -56,7 +56,7 @@ interface Props {
56
56
  disabled?: boolean;
57
57
  label: string;
58
58
  labelPlacement?: LabelPlacement;
59
- labelProps?: TypographyTypeMap['props'];
59
+ labelProps?: TypographyProps;
60
60
  onChange?: (e) => void;
61
61
  }
62
62
 
@@ -2,7 +2,7 @@ import { equals, includes } from 'ramda';
2
2
  import { makeStyles } from 'tss-react/mui';
3
3
 
4
4
  import FormGroup, { FormGroupProps } from '@mui/material/FormGroup';
5
- import { TypographyTypeMap } from '@mui/material/Typography';
5
+ import { TypographyProps } from '@mui/material/Typography';
6
6
 
7
7
  import Checkbox, { LabelPlacement } from '../Checkbox';
8
8
 
@@ -13,7 +13,7 @@ interface Props {
13
13
  disabled?: boolean;
14
14
  formGroupProps?: FormGroupProps;
15
15
  labelPlacement?: LabelPlacement;
16
- labelProps?: TypographyTypeMap['props'];
16
+ labelProps?: TypographyProps;
17
17
  onChange?: (e) => void;
18
18
  options: Array<string>;
19
19
  values: Array<string>;
@@ -27,7 +27,7 @@ interface DashboardItemProps {
27
27
  style?: CSSProperties;
28
28
  }
29
29
 
30
- const Item = forwardRef(
30
+ const Item = forwardRef<HTMLDivElement, DashboardItemProps>(
31
31
  (
32
32
  {
33
33
  children,
@@ -23,7 +23,7 @@ interface DashboardLayoutProps<T> {
23
23
  layout: Array<T>;
24
24
  }
25
25
 
26
- const Layout = <T extends Layout>({
26
+ const DashboardLayout = <T extends Layout>({
27
27
  children,
28
28
  changeLayout,
29
29
  displayGrid,
@@ -76,4 +76,4 @@ const Layout = <T extends Layout>({
76
76
  });
77
77
  };
78
78
 
79
- export default Layout;
79
+ export default DashboardLayout;
@@ -43,7 +43,7 @@ export type Props = {
43
43
  dialogTitleClassName?: string;
44
44
  labelCancel?: string | null;
45
45
  labelConfirm?: string | null;
46
- labelTitle?: string | null;
46
+ labelTitle?: ReactNode;
47
47
  onCancel?: () => void;
48
48
  onClose?: () => void;
49
49
  onConfirm: (event, value?) => void;
@@ -17,7 +17,7 @@ const useStyles = makeStyles()((theme) => ({
17
17
  logo: {
18
18
  alignSelf: 'flex-end',
19
19
  height: theme.spacing(11),
20
- width: '22rem'
20
+ width: '239px'
21
21
  },
22
22
  message: {
23
23
  color: theme.palette.text.primary
@@ -66,9 +66,9 @@ export const FallbackPage: FC<FallbackPageProps> = typedMemo(
66
66
 
67
67
  return (
68
68
  <div className={classes.notAuthorizedContainer}>
69
- <section className={classes.logo}>
69
+ <div className={classes.logo}>
70
70
  <CentreonLogo />
71
- </section>
71
+ </div>
72
72
  <section className={classes.messageBlock}>
73
73
  <header>
74
74
  <Typography color="primary" fontWeight="bold" variant="h3">
@@ -98,7 +98,9 @@ const getExtensions = cond([
98
98
  [T, identity]
99
99
  ]) as (accept: string) => Array<string>;
100
100
 
101
- export const transformFileListToArray = (files: FileList | null): Array<File> =>
101
+ export const transformFileListToArray = (
102
+ files: FileList | null
103
+ ): Array<File> =>
102
104
  isNil(files)
103
105
  ? []
104
106
  : (Array(files.length)
@@ -0,0 +1,133 @@
1
+ import { object } from 'yup';
2
+ import { faker } from '@faker-js/faker';
3
+ import { useFormikContext } from 'formik';
4
+
5
+ import { Typography } from '@mui/material';
6
+
7
+ import { Button } from '../components';
8
+
9
+ import { Form } from './Form';
10
+ import { InputType } from './Inputs/models';
11
+
12
+ faker.seed(42);
13
+
14
+ const AddItem = ({ addItem }: { addItem: (item) => void }): JSX.Element => {
15
+ const { values } = useFormikContext();
16
+ const add = (): void => {
17
+ addItem({
18
+ alias: faker.company.name(),
19
+ id: values.list.length,
20
+ name: faker.person.firstName()
21
+ });
22
+ };
23
+
24
+ return (
25
+ <Button variant="ghost" onClick={add}>
26
+ Add item
27
+ </Button>
28
+ );
29
+ };
30
+
31
+ const SortContent = ({
32
+ name,
33
+ alias
34
+ }: {
35
+ alias: string;
36
+ name: string;
37
+ }): JSX.Element => (
38
+ <Typography>
39
+ {name} ({alias})
40
+ </Typography>
41
+ );
42
+
43
+ const initializeFormList = (): void => {
44
+ cy.mount({
45
+ Component: (
46
+ <Form
47
+ initialValues={{
48
+ list: []
49
+ }}
50
+ inputs={[
51
+ {
52
+ fieldName: 'list',
53
+ group: '',
54
+ label: '',
55
+ list: {
56
+ AddItem,
57
+ SortContent,
58
+ addItemLabel: 'Add an item to the list',
59
+ itemProps: ['id', 'name', 'alias'],
60
+ sortLabel: 'Sort items'
61
+ },
62
+ type: InputType.List
63
+ }
64
+ ]}
65
+ submit={cy.stub()}
66
+ validationSchema={object()}
67
+ />
68
+ )
69
+ });
70
+ };
71
+
72
+ describe('Form list', () => {
73
+ beforeEach(initializeFormList);
74
+
75
+ it('adds an element to the list', () => {
76
+ cy.contains('Add an item to the list').should('be.visible');
77
+ cy.contains('Sort items').should('be.visible');
78
+
79
+ cy.contains('Add item').click();
80
+
81
+ cy.findByLabelText('sort-0').should('be.visible');
82
+ cy.findByLabelText('delete-0').should('be.visible');
83
+ cy.contains('Christelle (Schinner - Wiegand)').should('be.visible');
84
+
85
+ cy.makeSnapshot();
86
+ });
87
+
88
+ it('sorts elements in the list', () => {
89
+ cy.contains('Add an item to the list').should('be.visible');
90
+ cy.contains('Sort items').should('be.visible');
91
+
92
+ cy.contains('Add item').click();
93
+ cy.contains('Add item').click();
94
+
95
+ cy.findByLabelText('sort-0').should('be.visible');
96
+ cy.findByLabelText('delete-0').should('be.visible');
97
+ cy.contains('Carley (Satterfield, Miller and Metz)').should('be.visible');
98
+ cy.findByLabelText('sort-1').should('be.visible');
99
+ cy.findByLabelText('delete-1').should('be.visible');
100
+ cy.contains('Anderson (Crist - Bradtke)').should('be.visible');
101
+
102
+ cy.moveSortableElementUsingAriaLabel({
103
+ ariaLabel: 'sort-0',
104
+ direction: 'down'
105
+ });
106
+
107
+ cy.contains('Carley (Satterfield, Miller and Metz)').should('be.visible');
108
+ cy.contains('Anderson (Crist - Bradtke)').should('be.visible');
109
+
110
+ cy.makeSnapshot();
111
+ });
112
+
113
+ it('removes an element from the list', () => {
114
+ cy.contains('Add an item to the list').should('be.visible');
115
+ cy.contains('Sort items').should('be.visible');
116
+
117
+ cy.contains('Add item').click();
118
+ cy.contains('Add item').click();
119
+
120
+ cy.findByLabelText('sort-0').should('be.visible');
121
+ cy.findByLabelText('delete-0').should('be.visible');
122
+ cy.contains('Lea (Streich - Hartmann)').should('be.visible');
123
+ cy.findByLabelText('sort-1').should('be.visible');
124
+ cy.findByLabelText('delete-1').should('be.visible');
125
+ cy.contains('Akeem (Quigley LLC)').should('be.visible');
126
+
127
+ cy.findByLabelText('delete-0').click();
128
+
129
+ cy.contains('Lea (Streich - Hartmann)').should('not.exist');
130
+
131
+ cy.makeSnapshot();
132
+ });
133
+ });
@@ -46,10 +46,7 @@ const CheckboxGroup = ({
46
46
  return;
47
47
  }
48
48
 
49
- setFieldValue(
50
- fieldName,
51
- value?.filter((elm) => !equals(elm, label))
52
- );
49
+ setFieldValue(fieldName, value?.filter((elm) => !equals(elm, label)));
53
50
  };
54
51
 
55
52
  return useMemoComponent({
@@ -0,0 +1,62 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { DraggableSyntheticListeners } from '@dnd-kit/core';
4
+
5
+ import KrilinIndicatorIcon from '@mui/icons-material/DragIndicator';
6
+ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
7
+
8
+ import { IconButton } from '../../../components';
9
+
10
+ import { useListStyles } from './List.styles';
11
+
12
+ export interface ContentProps {
13
+ attributes;
14
+ children: ReactNode;
15
+ deleteItem: (id: string) => () => void;
16
+ id: string;
17
+ isDragging: boolean;
18
+ isInDragOverlay?: boolean;
19
+ itemRef: React.RefObject<HTMLDivElement>;
20
+ listeners: DraggableSyntheticListeners;
21
+ name: string;
22
+ style;
23
+ }
24
+
25
+ const Content = ({
26
+ listeners,
27
+ itemRef,
28
+ attributes,
29
+ style,
30
+ isDragging,
31
+ id,
32
+ children,
33
+ deleteItem
34
+ }: ContentProps): JSX.Element => {
35
+ const { classes } = useListStyles();
36
+
37
+ return (
38
+ <div
39
+ className={classes.content}
40
+ ref={itemRef}
41
+ {...attributes}
42
+ style={style}
43
+ >
44
+ <IconButton
45
+ data-dragging={isDragging}
46
+ size="small"
47
+ {...listeners}
48
+ aria-label={`sort-${id}`}
49
+ icon={<KrilinIndicatorIcon fontSize="small" />}
50
+ />
51
+ <div className={classes.innerContent}>{children}</div>
52
+ <IconButton
53
+ aria-label={`delete-${id}`}
54
+ icon={<DeleteOutlineIcon color="error" fontSize="small" />}
55
+ size="small"
56
+ onClick={deleteItem(id)}
57
+ />
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default Content;
@@ -0,0 +1,29 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useListStyles = makeStyles()((theme) => ({
4
+ content: {
5
+ '& [data-dragging="false"]': {
6
+ cursor: 'grab'
7
+ },
8
+ '& [data-dragging="true"]': {
9
+ cursor: 'grabbing'
10
+ },
11
+ alignItems: 'center',
12
+ borderBottom: `1px dashed ${theme.palette.action.disabledBackground}`,
13
+ display: 'flex',
14
+ flexDirection: 'row',
15
+ padding: theme.spacing(1, 0)
16
+ },
17
+ innerContent: {
18
+ flexGrow: 1
19
+ },
20
+ items: {
21
+ maxHeight: theme.spacing(16),
22
+ overflowY: 'auto'
23
+ },
24
+ list: {
25
+ display: 'flex',
26
+ flexDirection: 'column',
27
+ gap: theme.spacing(1)
28
+ }
29
+ }));
@@ -0,0 +1,58 @@
1
+ import { ComponentType } from 'react';
2
+
3
+ import { closestCenter } from '@dnd-kit/core';
4
+ import { verticalListSortingStrategy } from '@dnd-kit/sortable';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { InputPropsWithoutGroup } from '../models';
8
+ import { SortableItems, Subtitle } from '../../..';
9
+
10
+ import { useList } from './useList';
11
+ import { useListStyles } from './List.styles';
12
+ import Content, { ContentProps } from './Content';
13
+
14
+ const List = ({
15
+ list,
16
+ fieldName
17
+ }: InputPropsWithoutGroup): JSX.Element | null => {
18
+ const { t } = useTranslation();
19
+ const { classes } = useListStyles();
20
+
21
+ const { addItem, sortList, sortedList, deleteItem } = useList({ fieldName });
22
+
23
+ const { AddItem, addItemLabel, sortLabel, SortContent, itemProps } = list as {
24
+ AddItem: ComponentType<{ addItem }>;
25
+ SortContent: ComponentType;
26
+ addItemLabel?: string;
27
+ itemProps: Array<string>;
28
+ sortLabel?: string;
29
+ };
30
+
31
+ return (
32
+ <div className={classes.list}>
33
+ {addItemLabel && <Subtitle>{t(addItemLabel)}</Subtitle>}
34
+ <AddItem addItem={addItem} />
35
+ {sortLabel && <Subtitle>{t(sortLabel)}</Subtitle>}
36
+ <div className={classes.items}>
37
+ <SortableItems
38
+ updateSortableItemsOnItemsChange
39
+ // eslint-disable-next-line react/no-unstable-nested-components
40
+ Content={(props: Omit<ContentProps, 'children' | 'deleteItem'>) => (
41
+ <Content {...props} deleteItem={deleteItem}>
42
+ <SortContent {...props} />
43
+ </Content>
44
+ )}
45
+ collisionDetection={closestCenter}
46
+ itemProps={itemProps}
47
+ items={sortedList}
48
+ sortingStrategy={verticalListSortingStrategy}
49
+ onDragEnd={({ items }): void => {
50
+ sortList(items);
51
+ }}
52
+ />
53
+ </div>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default List;
@@ -0,0 +1,81 @@
1
+ import { useMemo, useRef } from 'react';
2
+
3
+ import { FormikValues, useFormikContext } from 'formik';
4
+ import {
5
+ append,
6
+ equals,
7
+ inc,
8
+ isEmpty,
9
+ pluck,
10
+ prop,
11
+ reject,
12
+ sortBy
13
+ } from 'ramda';
14
+
15
+ import { SelectEntry } from '../../..';
16
+
17
+ interface UseListState {
18
+ addItem: (newItem: SelectEntry) => void;
19
+ deleteItem: (id: string) => () => void;
20
+ sortList: (items: Array<string>) => void;
21
+ sortedList: Array<unknown>;
22
+ }
23
+
24
+ export const useList = ({ fieldName }): UseListState => {
25
+ const { values, setFieldValue } = useFormikContext<FormikValues>();
26
+ const maxOrder = useRef(0);
27
+
28
+ const list = values[fieldName];
29
+
30
+ const sortedList = useMemo(
31
+ () =>
32
+ sortBy(prop('order'), list).map(({ id, ...props }) => ({
33
+ id: `${id}`,
34
+ ...props
35
+ })),
36
+ [list]
37
+ );
38
+
39
+ const addItem = (newItem: SelectEntry): void => {
40
+ setFieldValue(
41
+ fieldName,
42
+ append(
43
+ {
44
+ ...newItem,
45
+ id: (newItem as SelectEntry).id as number,
46
+ order: inc(maxOrder.current)
47
+ },
48
+ list
49
+ )
50
+ );
51
+ };
52
+
53
+ const deleteItem = (id: string) => (): void => {
54
+ const newItems = reject((item) => equals(Number(id), item.id))(list);
55
+
56
+ setFieldValue(fieldName, newItems);
57
+ };
58
+
59
+ const sortList = (items: Array<string>): void => {
60
+ const newOrderedList = items.map((itemId, idx) => {
61
+ const item = sortedList.find(({ id }) => equals(id, itemId));
62
+
63
+ return {
64
+ ...item,
65
+ id: Number(item?.id),
66
+ order: inc(idx)
67
+ };
68
+ });
69
+
70
+ setFieldValue(fieldName, newOrderedList);
71
+ };
72
+
73
+ maxOrder.current = isEmpty(list) ? 0 : Math.max(...pluck('order', list));
74
+
75
+ return {
76
+ addItem,
77
+ deleteItem,
78
+ sortList,
79
+ sortedList
80
+ };
81
+ };