@centreon/ui 24.5.0 → 24.5.2

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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/Dashboard/Item.tsx +2 -11
  3. package/src/Dashboard/Layout.tsx +2 -4
  4. package/src/Graph/Tree/DescendantNodes.tsx +0 -1
  5. package/src/Graph/Tree/Links.tsx +2 -15
  6. package/src/Graph/Tree/Tree.cypress.spec.tsx +0 -24
  7. package/src/Graph/Tree/Tree.stories.tsx +1 -17
  8. package/src/Graph/Tree/models.ts +0 -3
  9. package/src/TopCounterElements/TopCounterLayout.tsx +4 -3
  10. package/src/TopCounterElements/useCloseOnLegacyPage.tsx +6 -9
  11. package/src/api/useGraphQuery/index.ts +1 -7
  12. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +13 -27
  13. package/src/components/Form/AccessRights/AccessRights.stories.tsx +19 -0
  14. package/src/components/Form/AccessRights/AccessRights.styles.ts +1 -1
  15. package/src/components/Form/AccessRights/AccessRights.tsx +5 -6
  16. package/src/components/Form/AccessRights/Actions/Actions.styles.ts +7 -3
  17. package/src/components/Form/AccessRights/Actions/Actions.tsx +32 -15
  18. package/src/components/Form/AccessRights/Actions/useActions.ts +37 -4
  19. package/src/components/Form/AccessRights/models.ts +3 -0
  20. package/src/components/Form/AccessRights/storiesData.ts +3 -0
  21. package/src/components/List/Item/ListItem.styles.ts +2 -2
  22. package/src/components/Zoom/Minimap.tsx +2 -4
  23. package/src/components/Zoom/Zoom.cypress.spec.tsx +13 -13
  24. package/src/components/Zoom/Zoom.tsx +1 -4
  25. package/src/components/Zoom/ZoomContent.tsx +2 -5
  26. package/src/components/index.ts +0 -1
  27. package/src/utils/index.ts +0 -1
  28. package/src/utils/usePluralizedTranslation.ts +3 -20
  29. package/src/components/Form/AccessRights/useAccessRightsChange.ts +0 -30
  30. package/src/components/Form/AccessRights/utils.ts +0 -18
  31. package/src/components/Tabs/Tab.styles.ts +0 -25
  32. package/src/components/Tabs/TabPanel.tsx +0 -22
  33. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -70
  34. package/src/components/Tabs/Tabs.stories.tsx +0 -55
  35. package/src/components/Tabs/Tabs.tsx +0 -55
  36. package/src/components/Tabs/index.ts +0 -6
  37. package/src/utils/resourcesStatusURL.ts +0 -166
  38. package/src/utils/usePluralizedTranslation.test.ts +0 -159
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.5.0",
3
+ "version": "24.5.2",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -15,7 +15,6 @@ import { useMemoComponent } from '../utils';
15
15
  import { useDashboardItemStyles } from './Dashboard.styles';
16
16
 
17
17
  interface DashboardItemProps {
18
- additionalMemoProps?: Array<unknown>;
19
18
  canMove?: boolean;
20
19
  children: ReactElement;
21
20
  className?: string;
@@ -40,8 +39,7 @@ const Item = forwardRef<HTMLDivElement, DashboardItemProps>(
40
39
  onTouchEnd,
41
40
  id,
42
41
  disablePadding = false,
43
- canMove = false,
44
- additionalMemoProps = []
42
+ canMove = false
45
43
  }: DashboardItemProps,
46
44
  ref: ForwardedRef<HTMLDivElement>
47
45
  ): ReactElement => {
@@ -94,14 +92,7 @@ const Item = forwardRef<HTMLDivElement, DashboardItemProps>(
94
92
  </Card>
95
93
  </div>
96
94
  ),
97
- memoProps: [
98
- style,
99
- className,
100
- header,
101
- theme.palette.mode,
102
- canMove,
103
- ...additionalMemoProps
104
- ]
95
+ memoProps: [style, className, header, theme.palette.mode, canMove]
105
96
  });
106
97
  }
107
98
  );
@@ -16,7 +16,6 @@ import Grid from './Grid';
16
16
  const ReactGridLayout = WidthProvider(GridLayout);
17
17
 
18
18
  interface DashboardLayoutProps<T> {
19
- additionalMemoProps?: Array<unknown>;
20
19
  changeLayout?: (newLayout: Array<Layout>) => void;
21
20
  children: Array<JSX.Element>;
22
21
  displayGrid?: boolean;
@@ -29,8 +28,7 @@ const DashboardLayout = <T extends Layout>({
29
28
  changeLayout,
30
29
  displayGrid,
31
30
  layout,
32
- isStatic = false,
33
- additionalMemoProps = []
31
+ isStatic = false
34
32
  }: DashboardLayoutProps<T>): JSX.Element => {
35
33
  const { classes } = useDashboardLayoutStyles(isStatic);
36
34
 
@@ -74,7 +72,7 @@ const DashboardLayout = <T extends Layout>({
74
72
  </ParentSize>
75
73
  </ResponsiveHeight>
76
74
  ),
77
- memoProps: [columns, layout, displayGrid, isStatic, ...additionalMemoProps]
75
+ memoProps: [columns, layout, displayGrid, isStatic]
78
76
  });
79
77
  };
80
78
 
@@ -65,7 +65,6 @@ const DescendantNodes = <TData extends BaseProp>({
65
65
  <Group key={key} left={left} top={top}>
66
66
  <foreignObject
67
67
  height={nodeSize.height}
68
- style={{ userSelect: 'none' }}
69
68
  width={nodeSize.width}
70
69
  x={-nodeSize.width / 2}
71
70
  y={-nodeSize.height / 2}
@@ -1,10 +1,5 @@
1
- import {
2
- LinkHorizontal,
3
- LinkHorizontalStep,
4
- LinkHorizontalLine
5
- } from '@visx/shape';
1
+ import { LinkHorizontal } from '@visx/shape';
6
2
  import { HierarchyPointLink } from '@visx/hierarchy/lib/types';
7
- import { always, cond, equals, T } from 'ramda';
8
3
 
9
4
  import { useTheme } from '@mui/material';
10
5
 
@@ -14,12 +9,6 @@ interface Props<TData> extends Pick<TreeProps<TData>, 'treeLink'> {
14
9
  links: Array<HierarchyPointLink<Node<TData>>>;
15
10
  }
16
11
 
17
- const getLinkComponent = cond([
18
- [equals('line'), always(LinkHorizontalLine)],
19
- [equals('step'), always(LinkHorizontalStep)],
20
- [T, always(LinkHorizontal)]
21
- ]);
22
-
23
12
  const Links = <TData extends BaseProp>({
24
13
  links,
25
14
  treeLink
@@ -35,12 +24,10 @@ const Links = <TData extends BaseProp>({
35
24
  .descendants()
36
25
  .map((ancestor) => ancestor.data.data.id);
37
26
 
38
- const LinkComponent = getLinkComponent(treeLink?.type);
39
-
40
27
  const key = `${link.source.data.data.id}-${link.source.data.data.name}-${ancestorIds}_${link.target.data.data.id}-${link.target.data.data.name}-${descendantIds}`;
41
28
 
42
29
  return (
43
- <LinkComponent
30
+ <LinkHorizontal
44
31
  data={link}
45
32
  data-testid={`${link.source.data.data.id}_to_${link.target.data.data.id}`}
46
33
  fill="none"
@@ -168,28 +168,4 @@ describe('Complex data tree', () => {
168
168
 
169
169
  cy.makeSnapshot();
170
170
  });
171
-
172
- it('displays the tree with step links when a prop is set', () => {
173
- initializeStandaloneTree({
174
- treeLink: {
175
- type: 'step'
176
- }
177
- });
178
-
179
- cy.contains('T').should('be.visible');
180
-
181
- cy.makeSnapshot();
182
- });
183
-
184
- it('displays the tree with line links when a prop is set', () => {
185
- initializeStandaloneTree({
186
- treeLink: {
187
- type: 'line'
188
- }
189
- });
190
-
191
- cy.contains('T').should('be.visible');
192
-
193
- cy.makeSnapshot();
194
- });
195
171
  });
@@ -82,21 +82,6 @@ export const WithDefaultExpandFilter: Story = {
82
82
  render: StandaloneTreeTemplate
83
83
  };
84
84
 
85
- export const WithStepLink: Story = {
86
- args: {
87
- children: SimpleContent,
88
- node: {
89
- height: 90,
90
- width: 90
91
- },
92
- tree: simpleData,
93
- treeLink: {
94
- type: 'step'
95
- }
96
- },
97
- render: StandaloneTreeTemplate
98
- };
99
-
100
85
  export const WithCustomLinks: Story = {
101
86
  args: {
102
87
  children: SimpleContent,
@@ -110,8 +95,7 @@ export const WithCustomLinks: Story = {
110
95
  getStrokeDasharray: ({ target }) =>
111
96
  target.status === 'ok' ? '5,5' : '0',
112
97
  getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
113
- getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2),
114
- type: 'line'
98
+ getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
115
99
  }
116
100
  },
117
101
  render: StandaloneTreeTemplate
@@ -14,8 +14,6 @@ export interface BaseProp {
14
14
  name: string;
15
15
  }
16
16
 
17
- export type Link = 'curve' | 'line' | 'step';
18
-
19
17
  export interface ChildrenProps<TData> {
20
18
  ancestors: Array<Node<TData>>;
21
19
  depth: number;
@@ -50,6 +48,5 @@ export interface TreeProps<TData> {
50
48
  ) => string | number | undefined;
51
49
  getStrokeOpacity?: (props: LinkProps<TData>) => string | number | undefined;
52
50
  getStrokeWidth?: (props: LinkProps<TData>) => string | number | undefined;
53
- type?: Link;
54
51
  };
55
52
  }
@@ -1,11 +1,11 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useState, useEffect } from 'react';
2
2
 
3
3
  import { makeStyles } from 'tss-react/mui';
4
4
 
5
- import ExpandLessIcon from '@mui/icons-material/ExpandLess';
6
5
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
7
- import type { SvgIcon } from '@mui/material';
6
+ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
8
7
  import { Badge, ClickAwayListener } from '@mui/material';
8
+ import type { SvgIcon } from '@mui/material';
9
9
 
10
10
  import useCloseOnLegacyPage from './useCloseOnLegacyPage';
11
11
 
@@ -115,6 +115,7 @@ const TopCounterLayout = ({
115
115
  }: TopCounterLayoutProps): JSX.Element => {
116
116
  const { classes, cx } = useStyles();
117
117
  const [toggled, setToggled] = useState(false);
118
+
118
119
  const subMenuId = title.replace(/[^A-Za-z]/, '-');
119
120
  useCloseOnLegacyPage({ setToggled });
120
121
 
@@ -1,6 +1,5 @@
1
1
  import { Dispatch, SetStateAction, useEffect } from 'react';
2
2
 
3
- import { isNil } from 'ramda';
4
3
  import { useLocation } from 'react-router-dom';
5
4
 
6
5
  interface Props {
@@ -15,23 +14,21 @@ const useCloseOnLegacyPage = ({ setToggled }: Props): void => {
15
14
  };
16
15
 
17
16
  useEffect(() => {
18
- const iframe = document.getElementById(
19
- 'main-content'
20
- ) as HTMLIFrameElement | null;
17
+ const iframe = document.getElementById('main-content') as HTMLIFrameElement;
21
18
 
22
- if (!isLegacyRoute || isNil(iframe)) {
19
+ if (!isLegacyRoute) {
23
20
  return () => undefined;
24
21
  }
25
22
 
26
23
  const closeSubMenuOnLegacyPage = (): void => {
27
- iframe?.contentWindow?.document?.addEventListener('click', closeSubMenu);
24
+ iframe.contentWindow?.document?.addEventListener('click', closeSubMenu);
28
25
  };
29
26
 
30
- iframe?.addEventListener('load', closeSubMenuOnLegacyPage);
27
+ iframe.addEventListener('load', closeSubMenuOnLegacyPage);
31
28
 
32
29
  return () => {
33
- iframe?.removeEventListener('load', closeSubMenuOnLegacyPage);
34
- iframe?.contentWindow?.document?.removeEventListener(
30
+ iframe.removeEventListener('load', closeSubMenuOnLegacyPage);
31
+ iframe.contentWindow?.document?.removeEventListener(
35
32
  'click',
36
33
  closeSubMenu
37
34
  );
@@ -24,7 +24,6 @@ interface CustomTimePeriod {
24
24
  interface UseMetricsQueryProps {
25
25
  baseEndpoint: string;
26
26
  bypassMetricsExclusion?: boolean;
27
- bypassQueryParams?: boolean;
28
27
  includeAllResources?: boolean;
29
28
  metrics: Array<Metric>;
30
29
  refreshCount?: number;
@@ -91,8 +90,7 @@ const useGraphQuery = ({
91
90
  timePeriodType: 1
92
91
  },
93
92
  refreshInterval = false,
94
- refreshCount,
95
- bypassQueryParams = false
93
+ refreshCount
96
94
  }: UseMetricsQueryProps): UseMetricsQueryState => {
97
95
  const timePeriodToUse = equals(timePeriod?.timePeriodType, -1)
98
96
  ? {
@@ -116,10 +114,6 @@ const useGraphQuery = ({
116
114
  isLoading
117
115
  } = useFetchQuery<PerformanceGraphData>({
118
116
  getEndpoint: () => {
119
- if (bypassQueryParams) {
120
- return baseEndpoint;
121
- }
122
-
123
117
  const endpoint = buildListingEndpoint({
124
118
  baseEndpoint,
125
119
  parameters: {
@@ -15,11 +15,11 @@ import {
15
15
 
16
16
  const initialize = ({
17
17
  initialValues = simpleAccessRights,
18
- loading = false
18
+ loading = false,
19
+ link = 'link'
19
20
  }): unknown => {
20
21
  const cancel = cy.stub();
21
22
  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}
50
51
  loading={loading}
51
52
  roles={roles}
52
53
  submit={save}
53
- onChange={change}
54
54
  />
55
55
  </Provider>
56
56
  </TestQueryProvider>
@@ -60,7 +60,6 @@ const initialize = ({
60
60
 
61
61
  return {
62
62
  cancel,
63
- change,
64
63
  save
65
64
  };
66
65
  };
@@ -75,12 +74,21 @@ describe('Access rights', () => {
75
74
  cy.findByLabelText('Add a contact').should('be.visible');
76
75
  cy.findByTestId('add_role').should('be.disabled');
77
76
  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
+
84
92
  it('displays the access rights with an empty list', () => {
85
93
  initialize({ initialValues: emptyAccessRights });
86
94
 
@@ -89,7 +97,7 @@ describe('Access rights', () => {
89
97
  cy.makeSnapshot();
90
98
  });
91
99
 
92
- it('displays the access rights list', () => {
100
+ it('displays the access rights with an empty list', () => {
93
101
  initialize({});
94
102
 
95
103
  simpleAccessRights.forEach(({ name, email, isContactGroup, role }) => {
@@ -362,26 +370,4 @@ describe('Access rights', () => {
362
370
 
363
371
  cy.makeSnapshot();
364
372
  });
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
- });
387
373
  });
@@ -47,6 +47,7 @@ export const Default: Story = {
47
47
  },
48
48
  initialValues: defaultAccessRights,
49
49
  labels,
50
+ link: 'link',
50
51
  roles,
51
52
  submit: () => undefined
52
53
  },
@@ -62,6 +63,7 @@ export const AccessRightsWithStates: Story = {
62
63
  },
63
64
  initialValues: accessRightsWithStates,
64
65
  labels,
66
+ link: 'link',
65
67
  roles,
66
68
  submit: () => undefined
67
69
  },
@@ -77,6 +79,22 @@ export const withEmptyState: Story = {
77
79
  },
78
80
  initialValues: emptyAccessRights,
79
81
  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,
80
98
  roles,
81
99
  submit: () => undefined
82
100
  },
@@ -92,6 +110,7 @@ export const loading: Story = {
92
110
  },
93
111
  initialValues: emptyAccessRights,
94
112
  labels,
113
+ link: 'link',
95
114
  loading: true,
96
115
  roles,
97
116
  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
- width: '100%'
8
+ maxWidth: '520px'
9
9
  }
10
10
  }));
@@ -8,17 +8,16 @@ 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';
12
11
  import { useAccessRightsInitValues } from './useAccessRightsInitValues';
13
12
 
14
13
  interface Props {
15
- cancel?: ({ dirty, values }) => void;
14
+ cancel: ({ dirty, values }) => void;
16
15
  endpoints: Endpoints;
17
16
  initialValues: Array<AccessRightInitialValues>;
18
17
  isSubmitting?: boolean;
19
18
  labels: Labels;
19
+ link?: string;
20
20
  loading?: boolean;
21
- onChange?: (values: Array<AccessRightInitialValues>) => void;
22
21
  roles: Array<SelectEntry>;
23
22
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
24
23
  }
@@ -29,14 +28,13 @@ export const AccessRights = ({
29
28
  endpoints,
30
29
  submit,
31
30
  cancel,
31
+ link,
32
32
  loading,
33
33
  labels,
34
- isSubmitting,
35
- onChange
34
+ isSubmitting
36
35
  }: Props): JSX.Element => {
37
36
  const { classes } = useAccessRightsStyles();
38
37
  const clear = useAccessRightsInitValues({ initialValues });
39
- useAccessRightsChange(onChange);
40
38
 
41
39
  return (
42
40
  <div className={classes.container}>
@@ -48,6 +46,7 @@ export const AccessRights = ({
48
46
  clear={clear}
49
47
  isSubmitting={isSubmitting}
50
48
  labels={labels.actions}
49
+ link={link}
51
50
  submit={submit}
52
51
  />
53
52
  </div>
@@ -1,10 +1,14 @@
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
+ },
4
9
  cancelAndSave: {
5
10
  display: 'flex',
6
- flexDirection: 'row',
7
- gap: theme.spacing(2),
8
- justifyContent: 'flex-end'
11
+ flex: 'row',
12
+ gap: theme.spacing(2)
9
13
  }
10
14
  }));
@@ -1,5 +1,6 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
 
3
+ import LinkIcon from '@mui/icons-material/Link';
3
4
  import { CircularProgress } from '@mui/material';
4
5
 
5
6
  import { Button } from '../../..';
@@ -9,10 +10,11 @@ import { useActions } from './useActions';
9
10
  import { useActionsStyles } from './Actions.styles';
10
11
 
11
12
  interface Props {
12
- cancel?: ({ dirty, values }) => void;
13
+ cancel: ({ dirty, values }) => void;
13
14
  clear: () => void;
14
15
  isSubmitting?: boolean;
15
16
  labels: Labels['actions'];
17
+ link?: string;
16
18
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
17
19
  }
18
20
 
@@ -20,15 +22,17 @@ const Actions = ({
20
22
  labels,
21
23
  cancel,
22
24
  submit,
25
+ link,
23
26
  isSubmitting,
24
27
  clear
25
28
  }: Props): JSX.Element => {
26
29
  const { t } = useTranslation();
27
30
  const { classes } = useActionsStyles();
28
31
 
29
- const { dirty, save, formattedValues } = useActions({
32
+ const { dirty, copyLink, save, formattedValues } = useActions({
30
33
  clear,
31
34
  labels,
35
+ link,
32
36
  submit
33
37
  });
34
38
 
@@ -37,8 +41,21 @@ const Actions = ({
37
41
  };
38
42
 
39
43
  return (
40
- <div className={classes.cancelAndSave}>
41
- {cancel && (
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}>
42
59
  <Button
43
60
  aria-label={t(labels.cancel)}
44
61
  variant="secondary"
@@ -46,17 +63,17 @@ const Actions = ({
46
63
  >
47
64
  {t(labels.cancel)}
48
65
  </Button>
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>
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>
60
77
  </div>
61
78
  );
62
79
  };
@@ -1,32 +1,64 @@
1
1
  import { useAtomValue } from 'jotai';
2
- import { equals } from 'ramda';
2
+ import { equals, omit } from 'ramda';
3
3
 
4
4
  import { initialValuesAtom, valuesAtom } from '../atoms';
5
- import { AccessRightInitialValues, Labels } from '../models';
6
- import { formatValue, formatValueForSubmition } from '../utils';
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
+ };
7
20
 
8
21
  interface Props {
9
22
  clear: () => void;
10
23
  labels: Labels['actions'];
24
+ link?: string;
11
25
  submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
12
26
  }
13
27
 
14
28
  interface UseActionsState {
29
+ copyLink: () => void;
15
30
  dirty: boolean;
16
31
  formattedValues: Array<AccessRightInitialValues>;
17
32
  save: () => void;
18
33
  }
19
34
 
20
- export const useActions = ({ submit, clear }: Props): UseActionsState => {
35
+ export const useActions = ({
36
+ link,
37
+ labels,
38
+ submit,
39
+ clear
40
+ }: Props): UseActionsState => {
21
41
  const values = useAtomValue(valuesAtom);
22
42
  const initialValues = useAtomValue(initialValuesAtom);
23
43
 
44
+ const { copy } = useCopyToClipboard({
45
+ errorMessage: labels.copyError,
46
+ successMessage: labels.copySuccess
47
+ });
48
+
24
49
  const formattedValues = values
25
50
  .filter(({ isRemoved }) => !isRemoved)
26
51
  .map(formatValue);
27
52
 
28
53
  const dirty = !equals(initialValues, formattedValues);
29
54
 
55
+ const copyLink = (): void => {
56
+ if (!link) {
57
+ return;
58
+ }
59
+ copy(link);
60
+ };
61
+
30
62
  const save = (): void => {
31
63
  submit(
32
64
  values.filter(({ isRemoved }) => !isRemoved).map(formatValueForSubmition)
@@ -39,6 +71,7 @@ export const useActions = ({ submit, clear }: Props): UseActionsState => {
39
71
  };
40
72
 
41
73
  return {
74
+ copyLink,
42
75
  dirty,
43
76
  formattedValues,
44
77
  save
@@ -16,6 +16,9 @@ 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;
19
22
  save: string;
20
23
  };
21
24
  add: {
@@ -105,6 +105,9 @@ 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',
108
111
  save: 'Save'
109
112
  },
110
113
  add: {