@centreon/ui 24.4.72 → 24.4.74

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 (131) hide show
  1. package/package.json +24 -23
  2. package/src/Checkbox/fonts/roboto-bold-webfont.ttf +0 -0
  3. package/src/Checkbox/fonts/roboto-bold-webfont.woff +0 -0
  4. package/src/Checkbox/fonts/roboto-bold-webfont.woff2 +0 -0
  5. package/src/Checkbox/fonts/roboto-light-webfont.ttf +0 -0
  6. package/src/Checkbox/fonts/roboto-light-webfont.woff +0 -0
  7. package/src/Checkbox/fonts/roboto-light-webfont.woff2 +0 -0
  8. package/src/Checkbox/fonts/roboto-medium-webfont.ttf +0 -0
  9. package/src/Checkbox/fonts/roboto-medium-webfont.woff +0 -0
  10. package/src/Checkbox/fonts/roboto-medium-webfont.woff2 +0 -0
  11. package/src/Checkbox/fonts/roboto-regular-webfont.ttf +0 -0
  12. package/src/Checkbox/fonts/roboto-regular-webfont.woff +0 -0
  13. package/src/Checkbox/fonts/roboto-regular-webfont.woff2 +0 -0
  14. package/src/Dashboard/Item.tsx +2 -11
  15. package/src/Dashboard/Layout.tsx +2 -4
  16. package/src/Dashboard/utils.ts +1 -1
  17. package/src/Form/Inputs/Grid.tsx +8 -4
  18. package/src/Form/Inputs/models.ts +15 -14
  19. package/src/Graph/BarStack/BarStack.cypress.spec.tsx +87 -9
  20. package/src/Graph/BarStack/BarStack.stories.tsx +4 -2
  21. package/src/Graph/BarStack/BarStack.styles.ts +59 -30
  22. package/src/Graph/BarStack/Graph.tsx +176 -0
  23. package/src/Graph/BarStack/GraphAndLegend.tsx +119 -0
  24. package/src/Graph/BarStack/ResponsiveBarStack.tsx +62 -157
  25. package/src/Graph/BarStack/constants.ts +5 -0
  26. package/src/Graph/BarStack/models.ts +1 -1
  27. package/src/Graph/BarStack/useGraphAndLegend.ts +86 -0
  28. package/src/Graph/BarStack/useResponsiveBarStack.ts +74 -99
  29. package/src/Graph/HeatMap/ResponsiveHeatMap.tsx +19 -2
  30. package/src/Graph/HeatMap/model.ts +5 -1
  31. package/src/Graph/Legend/Legend.styles.ts +10 -0
  32. package/src/Graph/Legend/Legend.tsx +6 -1
  33. package/src/Graph/LineChart/BasicComponents/Lines/RegularLines/index.tsx +2 -1
  34. package/src/Graph/LineChart/BasicComponents/Lines/StackedLines/index.tsx +3 -4
  35. package/src/Graph/LineChart/Icons/Downtime.tsx +3 -3
  36. package/src/Graph/LineChart/InteractiveComponents/ZoomPreview/index.tsx +2 -1
  37. package/src/Graph/LineChart/Legend/Legend.styles.ts +16 -5
  38. package/src/Graph/LineChart/Legend/LegendHeader.tsx +4 -1
  39. package/src/Graph/LineChart/Legend/index.tsx +12 -5
  40. package/src/Graph/LineChart/LineChart.cypress.spec.tsx +53 -0
  41. package/src/Graph/LineChart/LineChart.tsx +10 -9
  42. package/src/Graph/LineChart/index.stories.tsx +13 -0
  43. package/src/Graph/LineChart/mockedData/curvesWithSameColor.json +252 -0
  44. package/src/Graph/LineChart/useLineChartData.ts +68 -18
  45. package/src/Graph/PieChart/ResponsivePie.tsx +3 -1
  46. package/src/Graph/PieChart/models.ts +1 -0
  47. package/src/Graph/Tree/DescendantNodes.tsx +0 -1
  48. package/src/Graph/Tree/Links.tsx +2 -15
  49. package/src/Graph/Tree/Tree.cypress.spec.tsx +0 -24
  50. package/src/Graph/Tree/Tree.stories.tsx +1 -17
  51. package/src/Graph/Tree/models.ts +0 -3
  52. package/src/Graph/common/utils.ts +51 -2
  53. package/src/Icon/BaseIcon.tsx +32 -0
  54. package/src/Icon/DowntimeIcon.tsx +14 -0
  55. package/src/InputField/Select/Autocomplete/Connected/Multi/index.test.tsx +21 -1
  56. package/src/InputField/Select/Autocomplete/Connected/index.test.tsx +2 -2
  57. package/src/InputField/Select/Autocomplete/Connected/index.tsx +10 -7
  58. package/src/InputField/Select/Autocomplete/Multi/index.stories.tsx +19 -0
  59. package/src/InputField/Select/Autocomplete/Multi/index.tsx +8 -5
  60. package/src/InputField/Text/index.tsx +7 -5
  61. package/src/Listing/ActionBar/index.tsx +1 -0
  62. package/src/PopoverMenu/index.tsx +4 -4
  63. package/src/RichTextEditor/ContentEditable.tsx +195 -195
  64. package/src/StoryBookThemeProvider/index.tsx +35 -36
  65. package/src/ThemeProvider/index.tsx +12 -12
  66. package/src/ThemeProvider/palettes.ts +11 -8
  67. package/src/TimePeriods/CustomTimePeriod/PopoverCustomTimePeriod/PickersStartEndDate.tsx +2 -3
  68. package/src/TimePeriods/DateTimePickerInput.tsx +4 -1
  69. package/src/TopCounterElements/TopCounterLayout.tsx +4 -3
  70. package/src/TopCounterElements/useCloseOnLegacyPage.tsx +9 -6
  71. package/src/api/buildListingEndpoint/getSearchQueryParameterValue.ts +7 -1
  72. package/src/api/buildListingEndpoint/models.ts +1 -0
  73. package/src/api/useGraphQuery/index.ts +1 -7
  74. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +13 -27
  75. package/src/components/Form/AccessRights/AccessRights.stories.tsx +19 -0
  76. package/src/components/Form/AccessRights/AccessRights.styles.ts +1 -1
  77. package/src/components/Form/AccessRights/AccessRights.tsx +5 -6
  78. package/src/components/Form/AccessRights/Actions/Actions.styles.ts +7 -3
  79. package/src/components/Form/AccessRights/Actions/Actions.tsx +32 -15
  80. package/src/components/Form/AccessRights/Actions/useActions.ts +37 -4
  81. package/src/components/Form/AccessRights/models.ts +3 -0
  82. package/src/components/Form/AccessRights/storiesData.ts +3 -0
  83. package/src/components/List/Item/ListItem.styles.ts +2 -2
  84. package/src/components/Zoom/Minimap.tsx +2 -4
  85. package/src/components/Zoom/Zoom.cypress.spec.tsx +13 -13
  86. package/src/components/Zoom/Zoom.tsx +1 -4
  87. package/src/components/Zoom/ZoomContent.tsx +2 -5
  88. package/src/components/index.ts +0 -1
  89. package/src/fonts/roboto-bold-webfont.ttf +0 -0
  90. package/src/fonts/roboto-bold-webfont.woff +0 -0
  91. package/src/fonts/roboto-bold-webfont.woff2 +0 -0
  92. package/src/fonts/roboto-light-webfont.ttf +0 -0
  93. package/src/fonts/roboto-light-webfont.woff +0 -0
  94. package/src/fonts/roboto-light-webfont.woff2 +0 -0
  95. package/src/fonts/roboto-medium-webfont.ttf +0 -0
  96. package/src/fonts/roboto-medium-webfont.woff +0 -0
  97. package/src/fonts/roboto-medium-webfont.woff2 +0 -0
  98. package/src/fonts/roboto-regular-webfont.ttf +0 -0
  99. package/src/fonts/roboto-regular-webfont.woff +0 -0
  100. package/src/fonts/roboto-regular-webfont.woff2 +0 -0
  101. package/src/index.ts +1 -0
  102. package/src/utils/index.ts +25 -25
  103. package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +3 -0
  104. package/src/utils/useInfiniteScrollListing.ts +6 -1
  105. package/src/utils/useLocale/index.ts +10 -0
  106. package/src/utils/useLocale/useLocale.cypress.spec.tsx +40 -0
  107. package/src/utils/useLocaleDateTimeFormat/index.ts +5 -2
  108. package/src/utils/usePluralizedTranslation.ts +4 -21
  109. package/src/@assets/icons/downtime.icon.svg +0 -1
  110. package/src/components/Form/AccessRights/useAccessRightsChange.ts +0 -30
  111. package/src/components/Form/AccessRights/utils.ts +0 -18
  112. package/src/components/Tabs/Tab.styles.ts +0 -25
  113. package/src/components/Tabs/TabPanel.tsx +0 -22
  114. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -70
  115. package/src/components/Tabs/Tabs.stories.tsx +0 -55
  116. package/src/components/Tabs/Tabs.tsx +0 -55
  117. package/src/components/Tabs/index.ts +0 -6
  118. package/src/utils/resourcesStatusURL.ts +0 -166
  119. package/src/utils/usePluralizedTranslation.test.ts +0 -159
  120. /package/{public → src/Button}/fonts/roboto-bold-webfont.ttf +0 -0
  121. /package/{public → src/Button}/fonts/roboto-bold-webfont.woff +0 -0
  122. /package/{public → src/Button}/fonts/roboto-bold-webfont.woff2 +0 -0
  123. /package/{public → src/Button}/fonts/roboto-light-webfont.ttf +0 -0
  124. /package/{public → src/Button}/fonts/roboto-light-webfont.woff +0 -0
  125. /package/{public → src/Button}/fonts/roboto-light-webfont.woff2 +0 -0
  126. /package/{public → src/Button}/fonts/roboto-medium-webfont.ttf +0 -0
  127. /package/{public → src/Button}/fonts/roboto-medium-webfont.woff +0 -0
  128. /package/{public → src/Button}/fonts/roboto-medium-webfont.woff2 +0 -0
  129. /package/{public → src/Button}/fonts/roboto-regular-webfont.ttf +0 -0
  130. /package/{public → src/Button}/fonts/roboto-regular-webfont.woff +0 -0
  131. /package/{public → src/Button}/fonts/roboto-regular-webfont.woff2 +0 -0
@@ -1,31 +1,31 @@
1
- import * as React from 'react';
1
+ import { ReactNode, useMemo } from 'react';
2
2
 
3
3
  import { useAtomValue } from 'jotai';
4
- import { CSSInterpolation } from 'tss-react';
5
4
  import { equals } from 'ramda';
5
+ import { CSSInterpolation } from 'tss-react';
6
6
 
7
7
  import {
8
8
  ButtonProps,
9
- createTheme,
10
9
  InputBaseProps,
10
+ ThemeProvider as MuiThemeProvider,
11
11
  StyledEngineProvider,
12
12
  Theme,
13
- ThemeProvider as MuiThemeProvider
13
+ createTheme
14
14
  } from '@mui/material';
15
- import CssBaseline from '@mui/material/CssBaseline';
16
15
  import { autocompleteClasses } from '@mui/material/Autocomplete';
16
+ import CssBaseline from '@mui/material/CssBaseline';
17
17
  import { ThemeOptions } from '@mui/material/styles/createTheme';
18
18
 
19
19
  import { ThemeMode, userAtom } from '@centreon/ui-context';
20
- import RobotoLightWoff2 from '@centreon/ui/fonts/roboto-light-webfont.woff2';
21
- import RobotoRegularWoff2 from '@centreon/ui/fonts/roboto-regular-webfont.woff2';
22
- import RobotoMediumWoff2 from '@centreon/ui/fonts/roboto-medium-webfont.woff2';
23
- import RobotoBoldWoff2 from '@centreon/ui/fonts/roboto-bold-webfont.woff2';
20
+
21
+ import RobotoBoldWoff2 from '../fonts/roboto-bold-webfont.woff2';
22
+ import RobotoLightWoff2 from '../fonts/roboto-light-webfont.woff2';
23
+ import RobotoMediumWoff2 from '../fonts/roboto-medium-webfont.woff2';
24
+ import RobotoRegularWoff2 from '../fonts/roboto-regular-webfont.woff2';
24
25
 
25
26
  import { getPalette } from './palettes';
26
27
 
27
28
  declare module '@mui/styles/defaultTheme' {
28
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
29
29
  interface DefaultTheme extends Theme {}
30
30
  }
31
31
 
@@ -293,13 +293,13 @@ export const getTheme = (mode: ThemeMode): ThemeOptions => ({
293
293
  });
294
294
 
295
295
  interface Props {
296
- children: React.ReactNode;
296
+ children: ReactNode;
297
297
  }
298
298
 
299
299
  const ThemeProvider = ({ children }: Props): JSX.Element => {
300
300
  const { themeMode } = useAtomValue(userAtom);
301
301
 
302
- const theme = React.useMemo(
302
+ const theme = useMemo(
303
303
  () => createTheme(getTheme(themeMode || ThemeMode.light)),
304
304
  [themeMode]
305
305
  );
@@ -1,6 +1,9 @@
1
1
  import { equals } from 'ramda';
2
2
 
3
- import { alpha, PaletteOptions } from '@mui/material';
3
+ import {
4
+ type PaletteOptions as PaletteOptionsModel,
5
+ alpha
6
+ } from '@mui/material';
4
7
 
5
8
  import { ThemeMode } from '@centreon/ui-context';
6
9
 
@@ -140,9 +143,9 @@ declare module '@mui/material/Badge' {
140
143
  }
141
144
  }
142
145
 
143
- export const lightPalette: PaletteOptions = {
146
+ export const lightPalette: PaletteOptionsModel = {
144
147
  action: {
145
- acknowledged: '#67532C',
148
+ acknowledged: '#745F35',
146
149
  acknowledgedBackground: '#DFD2B9',
147
150
  activatedOpacity: 0.12,
148
151
  active: '#666666',
@@ -152,7 +155,7 @@ export const lightPalette: PaletteOptions = {
152
155
  focusOpacity: 0.12,
153
156
  hover: 'rgba(0, 0, 0, 0.06)',
154
157
  hoverOpacity: 0.06,
155
- inDowntime: '#4B2352',
158
+ inDowntime: '#512980',
156
159
  inDowntimeBackground: '#E5D8F3',
157
160
  selected: 'rgba(102, 102, 102, 0.3)',
158
161
  selectedOpacity: 0.3
@@ -280,9 +283,9 @@ export const lightPalette: PaletteOptions = {
280
283
  }
281
284
  };
282
285
 
283
- export const darkPalette: PaletteOptions = {
286
+ export const darkPalette: PaletteOptionsModel = {
284
287
  action: {
285
- acknowledged: '#67532C',
288
+ acknowledged: '#DFD2B9',
286
289
  acknowledgedBackground: '#745F35',
287
290
  activatedOpacity: 0.3,
288
291
  active: '#B5B5B5',
@@ -292,7 +295,7 @@ export const darkPalette: PaletteOptions = {
292
295
  focusOpacity: 0.3,
293
296
  hover: 'rgba(255, 255, 255, 0.16)',
294
297
  hoverOpacity: 0.16,
295
- inDowntime: '#4B2352',
298
+ inDowntime: '#E5D8F3',
296
299
  inDowntimeBackground: '#512980',
297
300
  selected: 'rgba(255, 255, 255, 0.5)',
298
301
  selectedOpacity: 0.5
@@ -418,5 +421,5 @@ export const darkPalette: PaletteOptions = {
418
421
  }
419
422
  };
420
423
 
421
- export const getPalette = (mode: ThemeMode): PaletteOptions =>
424
+ export const getPalette = (mode: ThemeMode): PaletteOptionsModel =>
422
425
  equals(mode, ThemeMode.dark) ? darkPalette : lightPalette;
@@ -10,14 +10,13 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
10
10
  import { Typography } from '@mui/material';
11
11
  import { LocalizationProvider } from '@mui/x-date-pickers';
12
12
 
13
- import { userAtom } from '@centreon/ui-context';
14
-
15
13
  import DateTimePickerInput from '../../DateTimePickerInput';
16
14
  import {
17
15
  CustomTimePeriodProperty,
18
16
  DateTimePickerInputModel
19
17
  } from '../../models';
20
18
  import { errorTimePeriodAtom } from '../../timePeriodsAtoms';
19
+ import { useLocale } from '../../../utils';
21
20
 
22
21
  import ErrorText from './ErrorText';
23
22
  import { PickersData, PickersStartEndDateDirection } from './models';
@@ -111,7 +110,7 @@ const PickersStartEndDate = ({
111
110
  }: Props): JSX.Element => {
112
111
  const { classes, cx } = useStyles();
113
112
 
114
- const { locale } = useAtomValue(userAtom);
113
+ const locale = useLocale();
115
114
  const error = useAtomValue(errorTimePeriodAtom);
116
115
 
117
116
  const maxStart = rangeStartDate?.max || endDate;
@@ -13,6 +13,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
13
13
 
14
14
  import { userAtom } from '@centreon/ui-context';
15
15
 
16
+ import { useLocale } from '../utils';
17
+
16
18
  import { CustomTimePeriodProperty } from './models';
17
19
 
18
20
  interface ChangeDateProps {
@@ -46,6 +48,7 @@ const DateTimePickerInput = ({
46
48
  '@media (min-width: 1024px) or (pointer: fine)';
47
49
 
48
50
  const { timezone, locale } = useAtomValue(userAtom);
51
+ const localeToUse = useLocale();
49
52
 
50
53
  const isUTC = equals(timezone, 'UTC');
51
54
 
@@ -62,7 +65,7 @@ const DateTimePickerInput = ({
62
65
 
63
66
  return (
64
67
  <LocalizationProvider
65
- adapterLocale={locale.substring(0, 2)}
68
+ adapterLocale={(locale ?? localeToUse).substring(0, 2)}
66
69
  dateAdapter={AdapterDayjs}
67
70
  dateLibInstance={dayjs}
68
71
  >
@@ -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,5 +1,6 @@
1
1
  import { Dispatch, SetStateAction, useEffect } from 'react';
2
2
 
3
+ import { isNil } from 'ramda';
3
4
  import { useLocation } from 'react-router-dom';
4
5
 
5
6
  interface Props {
@@ -14,21 +15,23 @@ const useCloseOnLegacyPage = ({ setToggled }: Props): void => {
14
15
  };
15
16
 
16
17
  useEffect(() => {
17
- const iframe = document.getElementById('main-content') as HTMLIFrameElement;
18
+ const iframe = document.getElementById(
19
+ 'main-content'
20
+ ) as HTMLIFrameElement | null;
18
21
 
19
- if (!isLegacyRoute) {
22
+ if (!isLegacyRoute || isNil(iframe)) {
20
23
  return () => undefined;
21
24
  }
22
25
 
23
26
  const closeSubMenuOnLegacyPage = (): void => {
24
- iframe.contentWindow?.document?.addEventListener('click', closeSubMenu);
27
+ iframe?.contentWindow?.document?.addEventListener('click', closeSubMenu);
25
28
  };
26
29
 
27
- iframe.addEventListener('load', closeSubMenuOnLegacyPage);
30
+ iframe?.addEventListener('load', closeSubMenuOnLegacyPage);
28
31
 
29
32
  return () => {
30
- iframe.removeEventListener('load', closeSubMenuOnLegacyPage);
31
- iframe.contentWindow?.document?.removeEventListener(
33
+ iframe?.removeEventListener('load', closeSubMenuOnLegacyPage);
34
+ iframe?.contentWindow?.document?.removeEventListener(
32
35
  'click',
33
36
  closeSubMenu
34
37
  );
@@ -93,8 +93,14 @@ const getConditionsSearchQueryParameterValue = (
93
93
  equals(listField, field)
94
94
  );
95
95
 
96
+ const globalOperator = filteredItems.every(({ operator }) =>
97
+ equals(operator, filteredItems[0].operator)
98
+ )
99
+ ? filteredItems[0].operator || '$or'
100
+ : '$or';
101
+
96
102
  return {
97
- $or: flatten(
103
+ [globalOperator]: flatten(
98
104
  filteredItems.map(({ value, values }) => {
99
105
  if (!isNil(value)) {
100
106
  return [
@@ -61,6 +61,7 @@ export type ConditionValue = {
61
61
 
62
62
  export interface ConditionsSearchParameter {
63
63
  field: string;
64
+ operator?: '$and' | '$or';
64
65
  value?: unknown;
65
66
  values?: ConditionValue;
66
67
  }
@@ -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: {