@centreon/ui 24.4.9 → 24.4.11

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": "@centreon/ui",
3
- "version": "24.4.9",
3
+ "version": "24.4.11",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -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'
@@ -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
+ });
@@ -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
+ };
@@ -21,6 +21,7 @@ import CheckboxGroup from './CheckboxGroup';
21
21
  import Checkbox from './Checkbox';
22
22
  import Custom from './Custom';
23
23
  import LoadingSkeleton from './LoadingSkeleton';
24
+ import List from './List/List';
24
25
 
25
26
  export const getInput = R.cond<
26
27
  Array<InputType>,
@@ -66,6 +67,7 @@ export const getInput = R.cond<
66
67
  R.equals(InputType.CheckboxGroup) as (b: InputType) => boolean,
67
68
  R.always(CheckboxGroup)
68
69
  ],
70
+ [R.equals(InputType.List) as (b: InputType) => boolean, R.always(List)],
69
71
  [R.T, R.always(TextInput)]
70
72
  ]);
71
73
 
@@ -18,7 +18,8 @@ export enum InputType {
18
18
  Grid,
19
19
  Custom,
20
20
  Checkbox,
21
- CheckboxGroup
21
+ CheckboxGroup,
22
+ List
22
23
  }
23
24
 
24
25
  interface FieldsTableGetRequiredProps {
@@ -76,6 +77,13 @@ export interface InputProps {
76
77
  hideInput?: (values: FormikValues) => boolean;
77
78
  inputClassName?: string;
78
79
  label: string;
80
+ list?: {
81
+ AddItem: React.ComponentType<{ addItem }>;
82
+ SortContent: React.ComponentType<object>;
83
+ addItemLabel?: string;
84
+ itemProps: Array<string>;
85
+ sortLabel?: string;
86
+ };
79
87
  radio?: {
80
88
  options?: Array<{
81
89
  label: string | JSX.Element;
@@ -77,9 +77,9 @@ const mountListing = (): void => {
77
77
  subItems={{
78
78
  canCheckSubItems: false,
79
79
  enable: true,
80
+ getRowProperty: () => 'subItems',
80
81
  labelCollapse: 'Collapse',
81
- labelExpand: 'Expand',
82
- rowProperty: 'subItems'
82
+ labelExpand: 'Expand'
83
83
  }}
84
84
  totalRows={10}
85
85
  />
@@ -387,9 +387,9 @@ export const ListingWithSubItems = {
387
387
  subItems: {
388
388
  canCheckSubItems: false,
389
389
  enable: true,
390
+ getRowProperty: () => 'subItems',
390
391
  labelCollapse: 'Collapse',
391
- labelExpand: 'Expand',
392
- rowProperty: 'subItems'
392
+ labelExpand: 'Expand'
393
393
  },
394
394
  totalRows: 10
395
395
  },
@@ -121,9 +121,9 @@ export interface Props<TRow> {
121
121
  subItems?: {
122
122
  canCheckSubItems: boolean;
123
123
  enable: boolean;
124
+ getRowProperty: (row?) => string;
124
125
  labelCollapse: string;
125
126
  labelExpand: string;
126
- rowProperty: string;
127
127
  };
128
128
  totalRows?: number;
129
129
  viewerModeConfiguration?: ViewerModeConfiguration;
@@ -176,9 +176,9 @@ const Listing = <TRow extends { id: RowId }>({
176
176
  subItems = {
177
177
  canCheckSubItems: false,
178
178
  enable: false,
179
+ getRowProperty: () => '',
179
180
  labelCollapse: 'Collapse',
180
- labelExpand: 'Expand',
181
- rowProperty: ''
181
+ labelExpand: 'Expand'
182
182
  }
183
183
  }: Props<TRow>): JSX.Element => {
184
184
  const currentVisibleColumns = getVisibleColumns({
@@ -211,10 +211,10 @@ const Listing = <TRow extends { id: RowId }>({
211
211
  ? reduce<TRow, Array<TRow>>(
212
212
  (acc, row): Array<TRow> => {
213
213
  if (
214
- row[subItems.rowProperty] &&
214
+ row[subItems.getRowProperty()] &&
215
215
  subItemsPivots.includes(row.id)
216
216
  ) {
217
- return [...acc, row, ...row[subItems.rowProperty]];
217
+ return [...acc, row, ...row[subItems.getRowProperty()]];
218
218
  }
219
219
 
220
220
  return [...acc, row];
@@ -449,7 +449,7 @@ const Listing = <TRow extends { id: RowId }>({
449
449
  reduce<TRow | number, Array<string | number>>(
450
450
  (acc, row) => [
451
451
  ...acc,
452
- ...pluck('id', row[subItems?.rowProperty || ''] || [])
452
+ ...pluck('id', row[subItems?.getRowProperty() || ''] || [])
453
453
  ],
454
454
  [],
455
455
  rows
@@ -620,7 +620,7 @@ const Listing = <TRow extends { id: RowId }>({
620
620
  listingVariant={listingVariant}
621
621
  row={row}
622
622
  rowColorConditions={rowColorConditions}
623
- subItemsRowProperty={subItems?.rowProperty}
623
+ subItemsRowProperty={subItems?.getRowProperty(row)}
624
624
  />
625
625
  ))}
626
626
  </ListingRow>
@@ -0,0 +1,55 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { type, equals } from 'ramda';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { Typography } from '@mui/material';
7
+ import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
8
+
9
+ import { Tooltip } from '../components';
10
+
11
+ interface Props {
12
+ children: ReactNode;
13
+ secondaryLabel?: string | Array<string>;
14
+ }
15
+
16
+ const Subtitle = ({ children, secondaryLabel }: Props): JSX.Element => {
17
+ const { t } = useTranslation();
18
+
19
+ const containsSeveralSecondaryLabels = equals(type(secondaryLabel), 'Array');
20
+
21
+ return (
22
+ <Typography variant="subtitle1">
23
+ <strong>{children}</strong>
24
+ {secondaryLabel && (
25
+ <Tooltip
26
+ aria-label={
27
+ containsSeveralSecondaryLabels
28
+ ? secondaryLabel[0]
29
+ : (secondaryLabel as string)
30
+ }
31
+ followCursor={false}
32
+ label={
33
+ containsSeveralSecondaryLabels ? (
34
+ <>
35
+ {secondaryLabel.map((label) => (
36
+ <p key={label}>{t(label)}</p>
37
+ ))}
38
+ </>
39
+ ) : (
40
+ t(secondaryLabel)
41
+ )
42
+ }
43
+ placement="right"
44
+ >
45
+ <HelpOutlineIcon
46
+ color="primary"
47
+ sx={{ ml: 1, verticalAlign: 'middle' }}
48
+ />
49
+ </Tooltip>
50
+ )}
51
+ </Typography>
52
+ );
53
+ };
54
+
55
+ export default Subtitle;
@@ -70,6 +70,7 @@ const useMutationQuery = <T extends object, TMeta>({
70
70
  onMutate,
71
71
  onError,
72
72
  onSuccess,
73
+ onSettled,
73
74
  baseEndpoint
74
75
  }: UseMutationQueryProps<T, TMeta>): UseMutationQueryState<T> => {
75
76
  const { showErrorMessage } = useSnackbar();
@@ -101,6 +102,7 @@ const useMutationQuery = <T extends object, TMeta>({
101
102
  },
102
103
  onError,
103
104
  onMutate,
105
+ onSettled,
104
106
  onSuccess
105
107
  });
106
108
 
@@ -49,14 +49,15 @@ const useStyles = makeStyles<{
49
49
  }
50
50
  },
51
51
  modalActions: {
52
- bottom: 0,
52
+ '&[data-fixed="true"]': {
53
+ position: 'fixed'
54
+ },
53
55
  bottom: theme.spacing(2),
54
56
  display: 'flex',
55
57
  flexDirection: 'row',
56
58
  gap: theme.spacing(2),
57
59
  justifyContent: 'flex-end',
58
- position: 'fixed',
59
- right: theme.spacing(2)
60
+ right: theme.spacing(2.5)
60
61
  },
61
62
  modalBody: {
62
63
  '& > p': {
@@ -8,6 +8,7 @@ export type ModalActionsProps = {
8
8
  children?: React.ReactNode;
9
9
  disabled?: boolean;
10
10
  isDanger?: boolean;
11
+ isFixed?: boolean;
11
12
  labels?: ModalActionsLabels;
12
13
  onCancel?: () => void;
13
14
  onConfirm?: () => void;
@@ -24,12 +25,13 @@ const ModalActions = ({
24
25
  onCancel,
25
26
  onConfirm,
26
27
  isDanger = false,
27
- disabled
28
+ disabled,
29
+ isFixed
28
30
  }: ModalActionsProps): ReactElement => {
29
31
  const { classes } = useStyles();
30
32
 
31
33
  return (
32
- <div className={classes.modalActions}>
34
+ <div className={classes.modalActions} data-fixed={isFixed}>
33
35
  {children || (
34
36
  <>
35
37
  <Button
package/src/index.ts CHANGED
@@ -164,3 +164,4 @@ export { default as TimePeriods } from './TimePeriods';
164
164
  export { default as SimpleCustomTimePeriod } from './TimePeriods/CustomTimePeriod/SimpleCustomTimePeriod';
165
165
  export { default as DateTimePickerInput } from './TimePeriods/DateTimePickerInput';
166
166
  export * from './ParentSize';
167
+ export { default as Subtitle } from './Typography/Subtitle';
@@ -19,3 +19,4 @@ export * from './useInfiniteScrollListing';
19
19
  export * from './useLicenseExpirationWarning';
20
20
  export * from './useRefreshInterval';
21
21
  export * from './centreonBaseURL';
22
+ export * from './usePluralizedTranslation';
@@ -0,0 +1,21 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import pluralize from 'pluralize';
3
+
4
+ interface TProps {
5
+ count: number;
6
+ label: string;
7
+ }
8
+
9
+ export const usePluralizedTranslation = (): {
10
+ pluralizedT: (props: TProps) => string;
11
+ } => {
12
+ const translation = useTranslation();
13
+
14
+ const pluralizedT = ({ label, count }: TProps): string => {
15
+ return pluralize(translation.t(label), count);
16
+ };
17
+
18
+ return {
19
+ pluralizedT
20
+ };
21
+ };