@centreon/ui 24.4.69 → 24.4.71

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.69",
3
+ "version": "24.4.71",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -15,11 +15,11 @@ import {
15
15
 
16
16
  const initialize = ({
17
17
  initialValues = simpleAccessRights,
18
- loading = false,
19
- link = 'link'
18
+ loading = false
20
19
  }): unknown => {
21
20
  const cancel = cy.stub();
22
21
  const save = cy.stub();
22
+ const change = cy.stub();
23
23
 
24
24
  cy.interceptAPIRequest({
25
25
  alias: 'getContacts',
@@ -47,10 +47,10 @@ const initialize = ({
47
47
  }}
48
48
  initialValues={initialValues}
49
49
  labels={labels}
50
- link={link}
51
50
  loading={loading}
52
51
  roles={roles}
53
52
  submit={save}
53
+ onChange={change}
54
54
  />
55
55
  </Provider>
56
56
  </TestQueryProvider>
@@ -60,6 +60,7 @@ const initialize = ({
60
60
 
61
61
  return {
62
62
  cancel,
63
+ change,
63
64
  save
64
65
  };
65
66
  };
@@ -74,21 +75,12 @@ describe('Access rights', () => {
74
75
  cy.findByLabelText('Add a contact').should('be.visible');
75
76
  cy.findByTestId('add_role').should('be.disabled');
76
77
  cy.findByTestId('add').should('be.disabled');
77
- cy.findByLabelText('Copy link').should('be.visible');
78
78
  cy.findByLabelText('Cancel').should('be.visible');
79
79
  cy.findByLabelText('Save').should('be.visible');
80
80
 
81
81
  cy.makeSnapshot();
82
82
  });
83
83
 
84
- it('displays the access rights without link', () => {
85
- initialize({ link: null });
86
-
87
- cy.findByLabelText('Copy link').should('not.exist');
88
-
89
- cy.makeSnapshot();
90
- });
91
-
92
84
  it('displays the access rights with an empty list', () => {
93
85
  initialize({ initialValues: emptyAccessRights });
94
86
 
@@ -97,7 +89,7 @@ describe('Access rights', () => {
97
89
  cy.makeSnapshot();
98
90
  });
99
91
 
100
- it('displays the access rights with an empty list', () => {
92
+ it('displays the access rights list', () => {
101
93
  initialize({});
102
94
 
103
95
  simpleAccessRights.forEach(({ name, email, isContactGroup, role }) => {
@@ -370,4 +362,26 @@ describe('Access rights', () => {
370
362
 
371
363
  cy.makeSnapshot();
372
364
  });
365
+
366
+ it('calls the change function when the corresponding prop is set and the form is updated', () => {
367
+ const { change } = initialize({});
368
+
369
+ cy.contains(labels.add.contact).click();
370
+ cy.findByLabelText(labels.add.autocompleteContact).click();
371
+
372
+ cy.waitForRequest('@getContacts');
373
+
374
+ cy.contains('Entity 10').click();
375
+
376
+ cy.findByTestId('add').click();
377
+
378
+ cy.contains('Entity 10').should('be.visible');
379
+
380
+ cy.findByTestId('role-Entity 10').should('have.value', 'viewer');
381
+ cy.contains(labels.list.added)
382
+ .should('be.visible')
383
+ .then(() => {
384
+ expect(change).to.have.callCount(2);
385
+ });
386
+ });
373
387
  });
@@ -47,7 +47,6 @@ export const Default: Story = {
47
47
  },
48
48
  initialValues: defaultAccessRights,
49
49
  labels,
50
- link: 'link',
51
50
  roles,
52
51
  submit: () => undefined
53
52
  },
@@ -63,7 +62,6 @@ export const AccessRightsWithStates: Story = {
63
62
  },
64
63
  initialValues: accessRightsWithStates,
65
64
  labels,
66
- link: 'link',
67
65
  roles,
68
66
  submit: () => undefined
69
67
  },
@@ -79,22 +77,6 @@ export const withEmptyState: Story = {
79
77
  },
80
78
  initialValues: emptyAccessRights,
81
79
  labels,
82
- link: 'link',
83
- roles,
84
- submit: () => undefined
85
- },
86
- render: Template
87
- };
88
-
89
- export const withoutLink: Story = {
90
- args: {
91
- cancel: () => undefined,
92
- endpoints: {
93
- contact: '/contact',
94
- contactGroup: '/contactGroup'
95
- },
96
- initialValues: defaultAccessRights,
97
- labels,
98
80
  roles,
99
81
  submit: () => undefined
100
82
  },
@@ -110,7 +92,6 @@ export const loading: Story = {
110
92
  },
111
93
  initialValues: emptyAccessRights,
112
94
  labels,
113
- link: 'link',
114
95
  loading: true,
115
96
  roles,
116
97
  submit: () => undefined
@@ -5,6 +5,6 @@ export const useAccessRightsStyles = makeStyles()((theme) => ({
5
5
  display: 'flex',
6
6
  flexDirection: 'column',
7
7
  gap: theme.spacing(3),
8
- maxWidth: '520px'
8
+ width: '100%'
9
9
  }
10
10
  }));
@@ -8,16 +8,17 @@ import Provider from './Provider';
8
8
  import ShareInput from './ShareInput/ShareInput';
9
9
  import Stats from './Stats/Stats';
10
10
  import { AccessRightInitialValues, Endpoints, Labels } from './models';
11
+ import { useAccessRightsChange } from './useAccessRightsChange';
11
12
  import { useAccessRightsInitValues } from './useAccessRightsInitValues';
12
13
 
13
14
  interface Props {
14
- cancel: ({ dirty, values }) => void;
15
+ cancel?: ({ dirty, values }) => void;
15
16
  endpoints: Endpoints;
16
17
  initialValues: Array<AccessRightInitialValues>;
17
18
  isSubmitting?: boolean;
18
19
  labels: Labels;
19
- link?: string;
20
20
  loading?: boolean;
21
+ onChange?: (values: Array<AccessRightInitialValues>) => void;
21
22
  roles: Array<SelectEntry>;
22
23
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
23
24
  }
@@ -28,13 +29,14 @@ export const AccessRights = ({
28
29
  endpoints,
29
30
  submit,
30
31
  cancel,
31
- link,
32
32
  loading,
33
33
  labels,
34
- isSubmitting
34
+ isSubmitting,
35
+ onChange
35
36
  }: Props): JSX.Element => {
36
37
  const { classes } = useAccessRightsStyles();
37
38
  const clear = useAccessRightsInitValues({ initialValues });
39
+ useAccessRightsChange(onChange);
38
40
 
39
41
  return (
40
42
  <div className={classes.container}>
@@ -46,7 +48,6 @@ export const AccessRights = ({
46
48
  clear={clear}
47
49
  isSubmitting={isSubmitting}
48
50
  labels={labels.actions}
49
- link={link}
50
51
  submit={submit}
51
52
  />
52
53
  </div>
@@ -1,14 +1,10 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
3
  export const useActionsStyles = makeStyles()((theme) => ({
4
- actions: {
5
- backgroundColor: theme.palette.background.paper,
6
- display: 'flex',
7
- justifyContent: 'space-between'
8
- },
9
4
  cancelAndSave: {
10
5
  display: 'flex',
11
- flex: 'row',
12
- gap: theme.spacing(2)
6
+ flexDirection: 'row',
7
+ gap: theme.spacing(2),
8
+ justifyContent: 'flex-end'
13
9
  }
14
10
  }));
@@ -1,6 +1,5 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
 
3
- import LinkIcon from '@mui/icons-material/Link';
4
3
  import { CircularProgress } from '@mui/material';
5
4
 
6
5
  import { Button } from '../../..';
@@ -10,11 +9,10 @@ import { useActions } from './useActions';
10
9
  import { useActionsStyles } from './Actions.styles';
11
10
 
12
11
  interface Props {
13
- cancel: ({ dirty, values }) => void;
12
+ cancel?: ({ dirty, values }) => void;
14
13
  clear: () => void;
15
14
  isSubmitting?: boolean;
16
15
  labels: Labels['actions'];
17
- link?: string;
18
16
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
19
17
  }
20
18
 
@@ -22,17 +20,15 @@ const Actions = ({
22
20
  labels,
23
21
  cancel,
24
22
  submit,
25
- link,
26
23
  isSubmitting,
27
24
  clear
28
25
  }: Props): JSX.Element => {
29
26
  const { t } = useTranslation();
30
27
  const { classes } = useActionsStyles();
31
28
 
32
- const { dirty, copyLink, save, formattedValues } = useActions({
29
+ const { dirty, save, formattedValues } = useActions({
33
30
  clear,
34
31
  labels,
35
- link,
36
32
  submit
37
33
  });
38
34
 
@@ -41,21 +37,8 @@ const Actions = ({
41
37
  };
42
38
 
43
39
  return (
44
- <div className={classes.actions}>
45
- {link ? (
46
- <Button
47
- aria-label={t(labels.copyLink)}
48
- icon={<LinkIcon />}
49
- iconVariant="start"
50
- variant="ghost"
51
- onClick={copyLink}
52
- >
53
- {t(labels.copyLink)}
54
- </Button>
55
- ) : (
56
- <div />
57
- )}
58
- <div className={classes.cancelAndSave}>
40
+ <div className={classes.cancelAndSave}>
41
+ {cancel && (
59
42
  <Button
60
43
  aria-label={t(labels.cancel)}
61
44
  variant="secondary"
@@ -63,17 +46,17 @@ const Actions = ({
63
46
  >
64
47
  {t(labels.cancel)}
65
48
  </Button>
66
- <Button
67
- aria-label={t(labels.save)}
68
- disabled={isSubmitting || !dirty}
69
- icon={isSubmitting ? <CircularProgress size={24} /> : null}
70
- iconVariant={isSubmitting ? 'start' : 'none'}
71
- variant="primary"
72
- onClick={save}
73
- >
74
- {t(labels.save)}
75
- </Button>
76
- </div>
49
+ )}
50
+ <Button
51
+ aria-label={t(labels.save)}
52
+ disabled={isSubmitting || !dirty}
53
+ icon={isSubmitting ? <CircularProgress size={24} /> : null}
54
+ iconVariant={isSubmitting ? 'start' : 'none'}
55
+ variant="primary"
56
+ onClick={save}
57
+ >
58
+ {t(labels.save)}
59
+ </Button>
77
60
  </div>
78
61
  );
79
62
  };
@@ -1,64 +1,32 @@
1
1
  import { useAtomValue } from 'jotai';
2
- import { equals, omit } from 'ramda';
2
+ import { equals } from 'ramda';
3
3
 
4
4
  import { initialValuesAtom, valuesAtom } from '../atoms';
5
- import { AccessRight, AccessRightInitialValues, Labels } from '../models';
6
- import { useCopyToClipboard } from '../../../..';
7
-
8
- const formatValue = (accessRight: AccessRight): AccessRightInitialValues => {
9
- return omit(['isAdded', 'isUpdated', 'isRemoved'], accessRight);
10
- };
11
-
12
- const formatValueForSubmition = (
13
- accessRight: AccessRight
14
- ): AccessRightInitialValues => {
15
- return {
16
- ...formatValue(accessRight),
17
- id: Number((accessRight.id as string).split('_')[1])
18
- };
19
- };
5
+ import { AccessRightInitialValues, Labels } from '../models';
6
+ import { formatValue, formatValueForSubmition } from '../utils';
20
7
 
21
8
  interface Props {
22
9
  clear: () => void;
23
10
  labels: Labels['actions'];
24
- link?: string;
25
11
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
26
12
  }
27
13
 
28
14
  interface UseActionsState {
29
- copyLink: () => void;
30
15
  dirty: boolean;
31
16
  formattedValues: Array<AccessRightInitialValues>;
32
17
  save: () => void;
33
18
  }
34
19
 
35
- export const useActions = ({
36
- link,
37
- labels,
38
- submit,
39
- clear
40
- }: Props): UseActionsState => {
20
+ export const useActions = ({ submit, clear }: Props): UseActionsState => {
41
21
  const values = useAtomValue(valuesAtom);
42
22
  const initialValues = useAtomValue(initialValuesAtom);
43
23
 
44
- const { copy } = useCopyToClipboard({
45
- errorMessage: labels.copyError,
46
- successMessage: labels.copySuccess
47
- });
48
-
49
24
  const formattedValues = values
50
25
  .filter(({ isRemoved }) => !isRemoved)
51
26
  .map(formatValue);
52
27
 
53
28
  const dirty = !equals(initialValues, formattedValues);
54
29
 
55
- const copyLink = (): void => {
56
- if (!link) {
57
- return;
58
- }
59
- copy(link);
60
- };
61
-
62
30
  const save = (): void => {
63
31
  submit(
64
32
  values.filter(({ isRemoved }) => !isRemoved).map(formatValueForSubmition)
@@ -71,7 +39,6 @@ export const useActions = ({
71
39
  };
72
40
 
73
41
  return {
74
- copyLink,
75
42
  dirty,
76
43
  formattedValues,
77
44
  save
@@ -16,9 +16,6 @@ export interface AccessRight extends AccessRightInitialValues {
16
16
  export interface Labels {
17
17
  actions: {
18
18
  cancel: string;
19
- copyError: string;
20
- copyLink: string;
21
- copySuccess: string;
22
19
  save: string;
23
20
  };
24
21
  add: {
@@ -105,9 +105,6 @@ export const buildResult = (isGroup): Listing<SelectEntry> => ({
105
105
  export const labels: Labels = {
106
106
  actions: {
107
107
  cancel: 'Cancel',
108
- copyError: 'Failed to copy',
109
- copyLink: 'Copy link',
110
- copySuccess: 'Copied',
111
108
  save: 'Save'
112
109
  },
113
110
  add: {
@@ -0,0 +1,30 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useAtomValue } from 'jotai';
4
+
5
+ import { useDeepCompare } from '../../../utils';
6
+
7
+ import { valuesAtom } from './atoms';
8
+ import { formatValueForSubmition } from './utils';
9
+ import { AccessRightInitialValues } from './models';
10
+
11
+ export const useAccessRightsChange = (
12
+ onChange?: (values: Array<AccessRightInitialValues>) => void
13
+ ): void => {
14
+ const values = useAtomValue(valuesAtom);
15
+
16
+ useEffect(
17
+ () => {
18
+ if (!onChange) {
19
+ return;
20
+ }
21
+
22
+ onChange(
23
+ values
24
+ .filter(({ isRemoved }) => !isRemoved)
25
+ .map(formatValueForSubmition)
26
+ );
27
+ },
28
+ useDeepCompare([values])
29
+ );
30
+ };
@@ -0,0 +1,18 @@
1
+ import { omit } from 'ramda';
2
+
3
+ import { AccessRight, AccessRightInitialValues } from './models';
4
+
5
+ export const formatValue = (
6
+ accessRight: AccessRight
7
+ ): AccessRightInitialValues => {
8
+ return omit(['isAdded', 'isUpdated', 'isRemoved'], accessRight);
9
+ };
10
+
11
+ export const formatValueForSubmition = (
12
+ accessRight: AccessRight
13
+ ): AccessRightInitialValues => {
14
+ return {
15
+ ...formatValue(accessRight),
16
+ id: Number((accessRight.id as string).split('_')[1])
17
+ };
18
+ };
@@ -18,10 +18,10 @@ export const useStyles = makeStyles()((theme) => ({
18
18
  display: 'flex',
19
19
  flexGrow: 1,
20
20
  gap: theme.spacing(2),
21
- maxWidth: '520px',
22
21
  overflow: 'hidden',
23
22
  paddingBottom: theme.spacing(1),
24
- paddingTop: theme.spacing(1)
23
+ paddingTop: theme.spacing(1),
24
+ width: '100%'
25
25
  },
26
26
  secondary: {
27
27
  alignItems: 'center',
@@ -0,0 +1,25 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useTabsStyles = makeStyles()((theme) => ({
4
+ indicator: {
5
+ bottom: 'unset'
6
+ },
7
+ tab: {
8
+ '&[aria-selected="true"]': {
9
+ color: theme.palette.text.primary,
10
+ fontWeight: theme.typography.fontWeightBold
11
+ },
12
+ color: theme.palette.text.primary,
13
+ fontWeight: theme.typography.fontWeightRegular,
14
+ marginRight: theme.spacing(2),
15
+ minHeight: 0,
16
+ minWidth: 0,
17
+ padding: theme.spacing(0.5, 0)
18
+ },
19
+ tabPanel: {
20
+ padding: theme.spacing(1, 0, 0)
21
+ },
22
+ tabs: {
23
+ minHeight: theme.spacing(4.5)
24
+ }
25
+ }));
@@ -0,0 +1,22 @@
1
+ import { TabPanel as MuiTabPanel } from '@mui/lab';
2
+
3
+ import { useTabsStyles } from './Tab.styles';
4
+
5
+ type Props = {
6
+ children: JSX.Element;
7
+ value: string;
8
+ };
9
+
10
+ export const TabPanel = ({ children, value }: Props): JSX.Element => {
11
+ const { classes } = useTabsStyles();
12
+
13
+ return (
14
+ <MuiTabPanel
15
+ className={classes.tabPanel}
16
+ data-tabPanel={value}
17
+ value={value}
18
+ >
19
+ {children}
20
+ </MuiTabPanel>
21
+ );
22
+ };
@@ -0,0 +1,70 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ import { TabPanel } from './TabPanel';
4
+
5
+ import { Tabs } from '.';
6
+
7
+ const initialize = (withTabListProps = false): void => {
8
+ cy.mount({
9
+ Component: (
10
+ <Tabs
11
+ defaultTab="tab 0"
12
+ tabList={
13
+ withTabListProps
14
+ ? {
15
+ variant: 'fullWidth'
16
+ }
17
+ : undefined
18
+ }
19
+ tabs={[
20
+ { label: 'Tab 0', value: 'tab 0' },
21
+ { label: 'Tab 1', value: 'tab 1' }
22
+ ]}
23
+ >
24
+ <TabPanel value="tab 0">
25
+ <Typography>Tab 0</Typography>
26
+ </TabPanel>
27
+ <TabPanel value="tab 1">
28
+ <Typography>Tab 1</Typography>
29
+ </TabPanel>
30
+ </Tabs>
31
+ )
32
+ });
33
+ };
34
+
35
+ describe('Tabs', () => {
36
+ it('displays tabs and their content when a tab is selected', () => {
37
+ initialize();
38
+
39
+ cy.get('[data-TabPanel="tab 0"]').should('not.have.attr', 'hidden');
40
+ cy.get('[data-TabPanel="tab 1"]').should('have.attr', 'hidden');
41
+ cy.findByLabelText('Tab 0')
42
+ .should('have.attr', 'aria-selected')
43
+ .and('equals', 'true');
44
+ cy.findByLabelText('Tab 1')
45
+ .should('have.attr', 'aria-selected')
46
+ .and('equals', 'false');
47
+
48
+ cy.contains('Tab 1').click();
49
+
50
+ cy.get('[data-TabPanel="tab 0"]').should('have.attr', 'hidden');
51
+ cy.get('[data-TabPanel="tab 1"]').should('not.have.attr', 'hidden');
52
+ cy.findByLabelText('Tab 0')
53
+ .should('have.attr', 'aria-selected')
54
+ .and('equals', 'false');
55
+ cy.findByLabelText('Tab 1')
56
+ .should('have.attr', 'aria-selected')
57
+ .and('equals', 'true');
58
+
59
+ cy.makeSnapshot();
60
+ });
61
+
62
+ it('displays tabs when tabList props are set', () => {
63
+ initialize(true);
64
+
65
+ cy.get('[data-TabPanel="tab 0"]').should('not.have.attr', 'hidden');
66
+ cy.get('[data-TabPanel="tab 1"]').should('have.attr', 'hidden');
67
+
68
+ cy.makeSnapshot();
69
+ });
70
+ });
@@ -0,0 +1,55 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Typography } from '@mui/material';
4
+
5
+ import { TabPanel } from './TabPanel';
6
+
7
+ import { Tabs } from '.';
8
+
9
+ const meta: Meta<typeof Tabs> = {
10
+ component: Tabs
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof Tabs>;
15
+
16
+ const generateTabs = (size: number): Array<{ label: string; value: string }> =>
17
+ Array(size)
18
+ .fill(0)
19
+ .map((_, index) => ({ label: `Tab ${index}`, value: `tab ${index}` }));
20
+
21
+ export const Default: Story = {
22
+ args: {
23
+ defaultTab: 'tab 0',
24
+ tabs: generateTabs(2)
25
+ },
26
+ render: (args) => (
27
+ <Tabs {...args}>
28
+ {generateTabs(2).map(({ value, label }) => (
29
+ <TabPanel key={value} value={value}>
30
+ <Typography>{label}</Typography>
31
+ </TabPanel>
32
+ ))}
33
+ </Tabs>
34
+ )
35
+ };
36
+
37
+ export const WithTabListProps: Story = {
38
+ args: {
39
+ defaultTab: 'tab 0',
40
+ tabList: {
41
+ textColor: 'inherit',
42
+ variant: 'fullWidth'
43
+ },
44
+ tabs: generateTabs(2)
45
+ },
46
+ render: (args) => (
47
+ <Tabs {...args}>
48
+ {generateTabs(2).map(({ value, label }) => (
49
+ <TabPanel key={value} value={value}>
50
+ <Typography>{label}</Typography>
51
+ </TabPanel>
52
+ ))}
53
+ </Tabs>
54
+ )
55
+ };
@@ -0,0 +1,55 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import { TabContext, TabList, TabListProps } from '@mui/lab';
4
+ import { Tab } from '@mui/material';
5
+
6
+ import { useTabsStyles } from './Tab.styles';
7
+
8
+ type Props = {
9
+ children: Array<JSX.Element>;
10
+ defaultTab: string;
11
+ tabList?: TabListProps;
12
+ tabs: Array<{
13
+ label: string;
14
+ value: string;
15
+ }>;
16
+ };
17
+
18
+ export const Tabs = ({
19
+ children,
20
+ defaultTab,
21
+ tabs,
22
+ tabList
23
+ }: Props): JSX.Element => {
24
+ const { classes } = useTabsStyles();
25
+
26
+ const [selectedTab, setSelectedTab] = useState(defaultTab);
27
+
28
+ const changeTab = useCallback((_, newValue: string): void => {
29
+ setSelectedTab(newValue);
30
+ }, []);
31
+
32
+ return (
33
+ <TabContext value={selectedTab}>
34
+ <TabList
35
+ classes={{
36
+ indicator: classes.indicator,
37
+ root: classes.tabs
38
+ }}
39
+ onChange={changeTab}
40
+ {...tabList}
41
+ >
42
+ {tabs.map(({ value, label }) => (
43
+ <Tab
44
+ aria-label={label}
45
+ className={classes.tab}
46
+ key={value}
47
+ label={label}
48
+ value={value}
49
+ />
50
+ ))}
51
+ </TabList>
52
+ {children}
53
+ </TabContext>
54
+ );
55
+ };
@@ -0,0 +1,6 @@
1
+ import { TabPanel } from './TabPanel';
2
+ import { Tabs as TabsRoot } from './Tabs';
3
+
4
+ export const Tabs = Object.assign(TabsRoot, {
5
+ TabPanel
6
+ });
@@ -12,3 +12,4 @@ export * from './Avatar';
12
12
  export * from './CollapsibleItem';
13
13
  export * from './Inputs';
14
14
  export { default as Zoom } from './Zoom/Zoom';
15
+ export * from './Tabs';
@@ -0,0 +1,159 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { useAtom } from 'jotai';
3
+
4
+ import { ListingVariant, userAtom } from '@centreon/ui-context';
5
+
6
+ import { usePluralizedTranslation } from './usePluralizedTranslation';
7
+
8
+ const baseUser = {
9
+ alias: 'admin',
10
+ isExportButtonEnabled: false,
11
+ name: 'admin',
12
+ timezone: 'Europe/Paris',
13
+ use_deprecated_pages: false,
14
+ user_interface_density: ListingVariant.compact
15
+ };
16
+
17
+ describe('usePluralizedTranslation', () => {
18
+ describe('English', () => {
19
+ it('returns the plural of a word when the corresponding count is set', () => {
20
+ const { result } = renderHook(() => {
21
+ return {
22
+ pluralizedTranslation: usePluralizedTranslation(),
23
+ userAtom: useAtom(userAtom)
24
+ };
25
+ });
26
+
27
+ act(() => {
28
+ result.current.userAtom[1]({
29
+ ...baseUser,
30
+ locale: 'en'
31
+ });
32
+ });
33
+
34
+ expect(
35
+ result.current.pluralizedTranslation.pluralizedT({
36
+ count: 2,
37
+ label: 'House'
38
+ })
39
+ ).toEqual('Houses');
40
+ });
41
+
42
+ it('returns the singular of a word when the corresponding count is set', () => {
43
+ const { result } = renderHook(() => {
44
+ return {
45
+ pluralizedTranslation: usePluralizedTranslation(),
46
+ userAtom: useAtom(userAtom)
47
+ };
48
+ });
49
+
50
+ act(() => {
51
+ result.current.userAtom[1]({
52
+ ...baseUser,
53
+ locale: 'en'
54
+ });
55
+ });
56
+
57
+ expect(
58
+ result.current.pluralizedTranslation.pluralizedT({
59
+ count: 1,
60
+ label: 'House'
61
+ })
62
+ ).toEqual('House');
63
+ });
64
+
65
+ it('returns the plural of a word when the corresponding count is 0 and the language is english', () => {
66
+ const { result } = renderHook(() => {
67
+ return {
68
+ pluralizedTranslation: usePluralizedTranslation(),
69
+ userAtom: useAtom(userAtom)
70
+ };
71
+ });
72
+
73
+ act(() => {
74
+ result.current.userAtom[1]({
75
+ ...baseUser,
76
+ locale: 'en'
77
+ });
78
+ });
79
+
80
+ expect(
81
+ result.current.pluralizedTranslation.pluralizedT({
82
+ count: 0,
83
+ label: 'House'
84
+ })
85
+ ).toEqual('Houses');
86
+ });
87
+ });
88
+
89
+ describe('French', () => {
90
+ it('returns the plural of a word when the corresponding count is set', () => {
91
+ const { result } = renderHook(() => {
92
+ return {
93
+ pluralizedTranslation: usePluralizedTranslation(),
94
+ userAtom: useAtom(userAtom)
95
+ };
96
+ });
97
+
98
+ act(() => {
99
+ result.current.userAtom[1]({
100
+ ...baseUser,
101
+ locale: 'fr'
102
+ });
103
+ });
104
+
105
+ expect(
106
+ result.current.pluralizedTranslation.pluralizedT({
107
+ count: 2,
108
+ label: 'Maison'
109
+ })
110
+ ).toEqual('Maisons');
111
+ });
112
+
113
+ it('returns the singular of a word when the corresponding count is set', () => {
114
+ const { result } = renderHook(() => {
115
+ return {
116
+ pluralizedTranslation: usePluralizedTranslation(),
117
+ userAtom: useAtom(userAtom)
118
+ };
119
+ });
120
+
121
+ act(() => {
122
+ result.current.userAtom[1]({
123
+ ...baseUser,
124
+ locale: 'fr'
125
+ });
126
+ });
127
+
128
+ expect(
129
+ result.current.pluralizedTranslation.pluralizedT({
130
+ count: 1,
131
+ label: 'Maison'
132
+ })
133
+ ).toEqual('Maison');
134
+ });
135
+
136
+ it('returns the singular of a word when the corresponding count is 0 and the language is english', () => {
137
+ const { result } = renderHook(() => {
138
+ return {
139
+ pluralizedTranslation: usePluralizedTranslation(),
140
+ userAtom: useAtom(userAtom)
141
+ };
142
+ });
143
+
144
+ act(() => {
145
+ result.current.userAtom[1]({
146
+ ...baseUser,
147
+ locale: 'fr'
148
+ });
149
+ });
150
+
151
+ expect(
152
+ result.current.pluralizedTranslation.pluralizedT({
153
+ count: 0,
154
+ label: 'Maison'
155
+ })
156
+ ).toEqual('Maison');
157
+ });
158
+ });
159
+ });
@@ -1,5 +1,11 @@
1
+ import { useCallback } from 'react';
2
+
1
3
  import { useTranslation } from 'react-i18next';
2
4
  import pluralize from 'pluralize';
5
+ import { useAtomValue } from 'jotai';
6
+ import { equals, includes } from 'ramda';
7
+
8
+ import { userAtom } from '@centreon/ui-context';
3
9
 
4
10
  interface TProps {
5
11
  count: number;
@@ -10,10 +16,21 @@ export const usePluralizedTranslation = (): {
10
16
  pluralizedT: (props: TProps) => string;
11
17
  } => {
12
18
  const translation = useTranslation();
19
+ const { locale } = useAtomValue(userAtom);
13
20
 
14
- const pluralizedT = ({ label, count }: TProps): string => {
15
- return pluralize(translation.t(label), count);
16
- };
21
+ const isNotPartitiveLocale = includes('fr', locale);
22
+
23
+ const pluralizedT = useCallback(
24
+ ({ label, count }: TProps): string => {
25
+ const isZero = equals(count, 0);
26
+
27
+ return pluralize(
28
+ translation.t(label),
29
+ isZero && isNotPartitiveLocale ? 1 : count
30
+ );
31
+ },
32
+ [isNotPartitiveLocale]
33
+ );
17
34
 
18
35
  return {
19
36
  pluralizedT