@centreon/ui 24.4.1-MON-34407-implement-the-status-chart-widget.29 → 24.4.1-MON-36983-dashboard-duplicate.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.4.1-MON-34407-implement-the-status-chart-widget.29",
3
+ "version": "24.4.1-MON-36983-dashboard-duplicate.2",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -29,7 +29,8 @@ const Text = ({
29
29
  getRequired,
30
30
  change,
31
31
  additionalMemoProps,
32
- text
32
+ text,
33
+ autoFocus
33
34
  }: InputPropsWithoutGroup): JSX.Element => {
34
35
  const { t } = useTranslation();
35
36
 
@@ -109,6 +110,7 @@ const Text = ({
109
110
  fullWidth
110
111
  EndAdornment={EndAdornment}
111
112
  ariaLabel={t(label) || ''}
113
+ autoFocus={autoFocus}
112
114
  dataTestId={dataTestId || ''}
113
115
  disabled={disabled}
114
116
  error={error as string | undefined}
@@ -33,6 +33,7 @@ export interface InputProps {
33
33
  additionalLabel?: string | JSX.Element;
34
34
  additionalLabelClassName?: string;
35
35
  additionalMemoProps?: Array<unknown>;
36
+ autoFocus?: boolean;
36
37
  autocomplete?: {
37
38
  creatable?: boolean;
38
39
  options: Array<SelectEntry>;
@@ -58,6 +58,11 @@ export const WithoutTitle: Story = {
58
58
  render: Template
59
59
  };
60
60
 
61
+ export const WithPencentage: Story = {
62
+ args: { data, title: 'hosts', unit: 'percentage' },
63
+ render: Template
64
+ };
65
+
61
66
  export const WithoutLegend: Story = {
62
67
  args: { data, displayLegend: false, title: 'hosts' },
63
68
 
@@ -69,11 +74,6 @@ export const withDisplayedValues: Story = {
69
74
  render: Template
70
75
  };
71
76
 
72
- export const WithPencentage: Story = {
73
- args: { data, displayValues: true, title: 'hosts', unit: 'percentage' },
74
- render: Template
75
- };
76
-
77
77
  export const WithTooltip: Story = {
78
78
  args: { TooltipContent, data, title: 'hosts' },
79
79
  render: Template
@@ -16,7 +16,7 @@ export const useBarStackStyles = makeStyles()((theme) => ({
16
16
  svgContainer: {
17
17
  alignItems: 'center',
18
18
  backgroundColor: theme.palette.background.panelGroups,
19
- borderRadius: theme.spacing(1.25),
19
+ borderRadius: theme.shape.borderRadius,
20
20
  display: 'flex',
21
21
  justifyContent: 'center'
22
22
  },
@@ -31,7 +31,6 @@ export const useBarStackStyles = makeStyles()((theme) => ({
31
31
  fontSize: theme.typography.h6.fontSize,
32
32
  fontWeight: theme.typography.fontWeightMedium,
33
33
  margin: 0,
34
- padding: 0,
35
- textAlign: 'center'
34
+ padding: 0
36
35
  }
37
36
  }));
@@ -5,7 +5,6 @@ import { Group } from '@visx/group';
5
5
  import numeral from 'numeral';
6
6
  import { Text } from '@visx/text';
7
7
  import { useTranslation } from 'react-i18next';
8
- import { equals } from 'ramda';
9
8
 
10
9
  import { Typography } from '@mui/material';
11
10
 
@@ -13,7 +12,7 @@ import { Tooltip } from '../../components';
13
12
  import { LegendProps } from '../Legend/models';
14
13
  import { Legend as LegendComponent } from '../Legend';
15
14
  import { getValueByUnit } from '../common/utils';
16
- import { labelNoDataFound as defaultlabelNoDataFound } from '../translatedLabels';
15
+ import { labelNoDataFound } from '../translatedLabels';
17
16
 
18
17
  import { BarStackProps } from './models';
19
18
  import { useBarStackStyles } from './BarStack.styles';
@@ -36,8 +35,7 @@ const BarStack = ({
36
35
  unit = 'number',
37
36
  displayValues,
38
37
  variant = 'vertical',
39
- legendDirection = 'column',
40
- labelNoDataFound = defaultlabelNoDataFound
38
+ legendDirection = 'column'
41
39
  }: BarStackProps & { height: number; width: number }): JSX.Element => {
42
40
  const { t } = useTranslation();
43
41
  const { classes } = useBarStackStyles();
@@ -92,7 +90,7 @@ const BarStack = ({
92
90
  >
93
91
  {title && (
94
92
  <div className={classes.title} data-testid="Title" ref={titleRef}>
95
- {`${numeral(total).format('0a').toUpperCase()} `} {t(title)}
93
+ {`${numeral(total).format('0a').toUpperCase()} `} {title}
96
94
  </div>
97
95
  )}
98
96
  <div
@@ -119,20 +117,6 @@ const BarStack = ({
119
117
  {(barStacks) =>
120
118
  barStacks.map((barStack) =>
121
119
  barStack.bars.map((bar) => {
122
- const shouldDisplayValues = (): boolean => {
123
- if (bar.height < 10) {
124
- return false;
125
- }
126
-
127
- return (
128
- (equals(unit, 'number') && bar.width > 10) ||
129
- (equals(unit, 'percentage') && bar.width > 25)
130
- );
131
- };
132
-
133
- const TextX = bar.x + bar.width / 2;
134
- const TextY = bar.y + bar.height / 2;
135
-
136
120
  const onClick = (): void => {
137
121
  onSingleBarClick?.(bar);
138
122
  };
@@ -160,38 +144,40 @@ const BarStack = ({
160
144
  isVerticalBar ? 'right-start' : 'bottom-start'
161
145
  }
162
146
  >
163
- <g data-testid={bar.key} onClick={onClick}>
147
+ <g data-testid={bar.key}>
164
148
  <rect
165
- cursor="pointer"
166
149
  fill={bar.color}
167
150
  height={
168
151
  isVerticalBar ? bar.height - 1 : bar.height
169
152
  }
170
153
  key={`bar-stack-${barStack.index}-${bar.index}`}
171
- ry={10}
154
+ ry={5}
172
155
  width={isVerticalBar ? bar.width : bar.width - 1}
173
156
  x={bar.x}
174
157
  y={bar.y}
158
+ onClick={onClick}
175
159
  />
176
- {displayValues && shouldDisplayValues() && (
177
- <Text
178
- cursor="pointer"
179
- data-testid="value"
180
- fill="#000"
181
- fontSize={12}
182
- fontWeight={600}
183
- textAnchor="middle"
184
- verticalAnchor="middle"
185
- x={TextX}
186
- y={TextY}
187
- >
188
- {getValueByUnit({
189
- total,
190
- unit,
191
- value: barStack.bars[0].bar.data[barStack.key]
192
- })}
193
- </Text>
194
- )}
160
+ {displayValues &&
161
+ bar.height > 10 &&
162
+ bar.width > 10 && (
163
+ <Text
164
+ cursor="pointer"
165
+ data-testid="value"
166
+ fill="#000"
167
+ fontSize={12}
168
+ textAnchor="middle"
169
+ verticalAnchor="middle"
170
+ x={bar.x + bar.width / 2}
171
+ y={bar.y + bar.height / 2}
172
+ >
173
+ {getValueByUnit({
174
+ total,
175
+ unit,
176
+ value:
177
+ barStack.bars[0].bar.data[barStack.key]
178
+ })}
179
+ </Text>
180
+ )}
195
181
  </g>
196
182
  </Tooltip>
197
183
  );
@@ -10,7 +10,6 @@ export type BarStackProps = {
10
10
  data: Array<BarType>;
11
11
  displayLegend?: boolean;
12
12
  displayValues?: boolean;
13
- labelNoDataFound?: string;
14
13
  legendDirection?: 'row' | 'column';
15
14
  onSingleBarClick?: (barData) => void;
16
15
  size?: number;
@@ -55,7 +55,7 @@ const useResponsiveBarStack = ({
55
55
  const verticalGap = heightOfTitle > 0 ? 8 : 0;
56
56
 
57
57
  const svgWrapperWidth = isVerticalBar
58
- ? size + 36
58
+ ? size + 24
59
59
  : width - widthOfLegend - horizontalGap;
60
60
 
61
61
  const svgContainerSize = {
@@ -72,7 +72,8 @@ const useResponsiveBarStack = ({
72
72
 
73
73
  const yScale = isVerticalBar
74
74
  ? scaleLinear({
75
- domain: [0, total]
75
+ domain: [0, total],
76
+ nice: true
76
77
  })
77
78
  : scaleBand({
78
79
  domain: [0, 0],
@@ -85,7 +86,8 @@ const useResponsiveBarStack = ({
85
86
  padding: 0
86
87
  })
87
88
  : scaleLinear({
88
- domain: [0, total]
89
+ domain: [0, total],
90
+ nice: true
89
91
  });
90
92
 
91
93
  const keys = pluck('label', data);
@@ -13,7 +13,7 @@ import { Tooltip } from '../../components';
13
13
  import { Legend as LegendComponent } from '../Legend';
14
14
  import { LegendProps } from '../Legend/models';
15
15
  import { getValueByUnit } from '../common/utils';
16
- import { labelNoDataFound as defaultlabelNoDataFound } from '../translatedLabels';
16
+ import { labelNoDataFound } from '../translatedLabels';
17
17
 
18
18
  import { PieProps } from './models';
19
19
  import { usePieStyles } from './PieChart.styles';
@@ -54,8 +54,7 @@ const ResponsivePie = ({
54
54
  onArcClick,
55
55
  displayValues,
56
56
  TooltipContent,
57
- legendDirection = 'column',
58
- labelNoDataFound = defaultlabelNoDataFound
57
+ legendDirection = 'column'
59
58
  }: PieProps & { height: number; width: number }): JSX.Element => {
60
59
  const { t } = useTranslation();
61
60
  const theme = useTheme();
@@ -110,7 +109,7 @@ const ResponsivePie = ({
110
109
  >
111
110
  {equals(variant, 'pie') && title && (
112
111
  <div className={classes.title} data-testid="Title" ref={titleRef}>
113
- {`${numeral(total).format('0a').toUpperCase()} `} {t(title)}
112
+ {`${numeral(total).format('0a').toUpperCase()} `} {title}
114
113
  </div>
115
114
  )}
116
115
  <div
@@ -179,7 +178,6 @@ const ResponsivePie = ({
179
178
  >
180
179
  <g data-testid={arc.data.label} onClick={onClick}>
181
180
  <path
182
- cursor="pointer"
183
181
  d={pie.path(arc) as string}
184
182
  fill={arc.data.color}
185
183
  />
@@ -191,7 +189,6 @@ const ResponsivePie = ({
191
189
  dy=".33em"
192
190
  fill="#000"
193
191
  fontSize={12}
194
- fontWeight={600}
195
192
  pointerEvents="none"
196
193
  textAnchor="middle"
197
194
  x={x}
@@ -227,7 +224,7 @@ const ResponsivePie = ({
227
224
  fill={theme.palette.text.primary}
228
225
  textAnchor="middle"
229
226
  >
230
- {t(title)}
227
+ {title}
231
228
  </Text>
232
229
  </>
233
230
  )}
@@ -11,7 +11,6 @@ export interface PieProps {
11
11
  displayLegend?: boolean;
12
12
  displayValues?: boolean;
13
13
  innerRadius?: number;
14
- labelNoDataFound?: string;
15
14
  legendDirection?: 'row' | 'column';
16
15
  onArcClick?: (ardata) => void;
17
16
  title?: string;
@@ -41,11 +41,12 @@ export const useResponsivePie = ({
41
41
  const horizontalGap = widthOfLegend > 0 ? 16 : 0;
42
42
  const verticalGap = heightOfTitle > 0 ? 8 : 0;
43
43
 
44
+ const svgWrapperWidth = width - widthOfLegend - horizontalGap;
45
+
44
46
  const svgContainerSize = Math.min(
45
47
  height - heightOfTitle - verticalGap,
46
48
  width - widthOfLegend - horizontalGap
47
49
  );
48
- const svgWrapperWidth = svgContainerSize;
49
50
 
50
51
  const outerRadius = Math.min(32, svgContainerSize / 6);
51
52
 
@@ -12,11 +12,7 @@ const data = Array(5)
12
12
  title: `Entity ${idx}`
13
13
  }));
14
14
 
15
- const initializeDataTableGrid = ({
16
- hasActions,
17
- hasCardAction,
18
- canDelete
19
- }): void => {
15
+ const initializeDataTableGrid = ({ hasActions, hasCardAction }): void => {
20
16
  cy.viewport(1200, 590);
21
17
  cy.mount({
22
18
  Component: (
@@ -27,14 +23,18 @@ const initializeDataTableGrid = ({
27
23
  hasActions={hasActions}
28
24
  hasCardAction={hasCardAction}
29
25
  key={title}
30
- labelsDelete={{
31
- cancel: 'Cancel',
32
- confirm: {
33
- label: 'Delete'
34
- }
26
+ labels={{
27
+ labelDelete: 'Delete',
28
+ labelDuplicate: 'Duplicate',
29
+ labelEditProperties: 'Edit Properties',
30
+ labelMoreActions: 'More actions',
31
+ labelShareWithContacts: 'Share with contacts'
35
32
  }}
36
33
  title={title}
37
- onDelete={canDelete ? cy.stub() : undefined}
34
+ onDelete={() => cy.stub()}
35
+ onDuplicate={() => cy.stub()}
36
+ onEdit={() => cy.stub()}
37
+ onEditAccessRights={() => cy.stub()}
38
38
  />
39
39
  ))}
40
40
  </DataTable>
@@ -94,7 +94,6 @@ const initializeDataTableListing = (): void => {
94
94
  describe('DataTable: Grid', () => {
95
95
  it('displays items with title and description only', () => {
96
96
  initializeDataTableGrid({
97
- canDelete: false,
98
97
  hasActions: false,
99
98
  hasCardAction: false
100
99
  });
@@ -109,32 +108,33 @@ describe('DataTable: Grid', () => {
109
108
 
110
109
  it('displays items with actions', () => {
111
110
  initializeDataTableGrid({
112
- canDelete: false,
113
111
  hasActions: true,
114
112
  hasCardAction: false
115
113
  });
116
114
 
117
- cy.findAllByLabelText('edit access rights').should('have.length', 5);
118
- cy.findAllByLabelText('edit').should('have.length', 5);
115
+ cy.findAllByLabelText('Share with contacts').should('have.length', 5);
116
+ cy.findAllByLabelText('More actions').should('have.length', 5);
119
117
 
120
118
  cy.makeSnapshot();
121
119
  });
122
120
 
123
- it('displays items with delete action', () => {
121
+ it('displays actions when more actions button was clicked', () => {
124
122
  initializeDataTableGrid({
125
- canDelete: true,
126
123
  hasActions: true,
127
124
  hasCardAction: false
128
125
  });
129
126
 
130
- cy.findAllByLabelText('delete').should('have.length', 5);
127
+ cy.findAllByLabelText('More actions').eq(0).click();
128
+
129
+ cy.findByLabelText('Duplicate').should('be.visible');
130
+ cy.findByLabelText('Delete').should('be.visible');
131
+ cy.findByLabelText('Edit Properties').should('be.visible');
131
132
 
132
133
  cy.makeSnapshot();
133
134
  });
134
135
 
135
136
  it('displays items with card action only', () => {
136
137
  initializeDataTableGrid({
137
- canDelete: false,
138
138
  hasActions: false,
139
139
  hasCardAction: true
140
140
  });
@@ -146,15 +146,13 @@ describe('DataTable: Grid', () => {
146
146
 
147
147
  it('displays items with card action and bottom actions', () => {
148
148
  initializeDataTableGrid({
149
- canDelete: true,
150
149
  hasActions: true,
151
150
  hasCardAction: true
152
151
  });
153
152
 
154
153
  cy.findAllByLabelText('view').should('have.length', 5);
155
- cy.findAllByLabelText('delete').should('have.length', 5);
156
- cy.findAllByLabelText('edit access rights').should('have.length', 5);
157
- cy.findAllByLabelText('edit').should('have.length', 5);
154
+ cy.findAllByLabelText('Share with contacts').should('have.length', 5);
155
+ cy.findAllByLabelText('More actions').should('have.length', 5);
158
156
 
159
157
  cy.makeSnapshot();
160
158
  });
@@ -1,6 +1,15 @@
1
- import React, { forwardRef, ReactElement, RefObject, useMemo } from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ ReactElement,
4
+ RefObject,
5
+ useMemo,
6
+ useState
7
+ } from 'react';
8
+
9
+ import { pipe } from 'ramda';
2
10
 
3
11
  import {
12
+ Menu,
4
13
  Card as MuiCard,
5
14
  CardActionArea as MuiCardActionArea,
6
15
  CardActions as MuiCardActions,
@@ -10,10 +19,12 @@ import {
10
19
  import {
11
20
  Delete as DeleteIcon,
12
21
  Settings as SettingsIcon,
13
- Share as ShareIcon
22
+ Share as ShareIcon,
23
+ ContentCopy as DuplicateIcon,
24
+ MoreHoriz as MoreIcon
14
25
  } from '@mui/icons-material';
15
26
 
16
- import { IconButton } from '../../Button';
27
+ import { IconButton, ActionsList, ActionsListActionDivider } from '../../..';
17
28
 
18
29
  import { useStyles } from './DataTableItem.styles';
19
30
 
@@ -21,16 +32,19 @@ export interface DataTableItemProps {
21
32
  description?: string;
22
33
  hasActions?: boolean;
23
34
  hasCardAction?: boolean;
35
+ labels;
24
36
  onClick?: () => void;
25
- onDelete?: () => void;
26
- onEdit?: () => void;
27
- onEditAccessRights?: () => void;
37
+ onDelete: () => void;
38
+ onDuplicate: () => void;
39
+ onEdit: () => void;
40
+ onEditAccessRights: () => void;
28
41
  title: string;
29
42
  }
30
43
 
31
44
  const DataTableItem = forwardRef(
32
45
  (
33
46
  {
47
+ labels,
34
48
  title,
35
49
  description,
36
50
  hasCardAction = false,
@@ -38,11 +52,16 @@ const DataTableItem = forwardRef(
38
52
  onClick,
39
53
  onEdit,
40
54
  onDelete,
55
+ onDuplicate,
41
56
  onEditAccessRights
42
57
  }: DataTableItemProps,
43
58
  ref
44
59
  ): ReactElement => {
45
60
  const { classes } = useStyles();
61
+ const [moreActionsOpen, setMoreActionsOpen] = useState(null);
62
+
63
+ const closeMoreActions = (): void => setMoreActionsOpen(null);
64
+ const openMoreActions = (event): void => setMoreActionsOpen(event.target);
46
65
 
47
66
  const ActionArea = useMemo(
48
67
  () => (hasCardAction ? MuiCardActionArea : React.Fragment),
@@ -66,33 +85,49 @@ const DataTableItem = forwardRef(
66
85
  </ActionArea>
67
86
  {hasActions && (
68
87
  <MuiCardActions>
88
+ <span />
69
89
  <span>
70
90
  <IconButton
71
- aria-label="delete"
72
- data-testid="delete"
73
- icon={<DeleteIcon />}
74
- size="small"
75
- variant="ghost"
76
- onClick={onDelete}
77
- />
78
- </span>
79
- <span>
80
- <IconButton
81
- aria-label="edit access rights"
82
- data-testid="edit-access-rights"
83
- icon={<ShareIcon />}
84
- size="small"
85
- variant="primary"
86
- onClick={() => onEditAccessRights?.()}
87
- />
91
+ ariaLabel={labels.labelShareWithContacts}
92
+ title={labels.labelShareWithContacts}
93
+ onClick={pipe(onEditAccessRights, closeMoreActions)}
94
+ >
95
+ <ShareIcon fontSize="small" />
96
+ </IconButton>
88
97
  <IconButton
89
- aria-label="edit"
90
- data-testid="edit"
91
- icon={<SettingsIcon />}
92
- size="small"
93
- variant="primary"
94
- onClick={() => onEdit?.()}
95
- />
98
+ ariaLabel={labels.labelMoreActions}
99
+ title={labels.labelMoreActions}
100
+ onClick={openMoreActions}
101
+ >
102
+ <MoreIcon />
103
+ </IconButton>
104
+ <Menu
105
+ anchorEl={moreActionsOpen}
106
+ open={Boolean(moreActionsOpen)}
107
+ onClose={closeMoreActions}
108
+ >
109
+ <ActionsList
110
+ actions={[
111
+ {
112
+ Icon: SettingsIcon,
113
+ label: labels.labelEditProperties,
114
+ onClick: pipe(onEdit, closeMoreActions)
115
+ },
116
+ ActionsListActionDivider.divider,
117
+ {
118
+ Icon: DuplicateIcon,
119
+ label: labels.labelDuplicate,
120
+ onClick: pipe(onDuplicate, closeMoreActions)
121
+ },
122
+ ActionsListActionDivider.divider,
123
+ {
124
+ Icon: DeleteIcon,
125
+ label: labels.labelDelete,
126
+ onClick: pipe(onDelete, closeMoreActions)
127
+ }
128
+ ]}
129
+ />
130
+ </Menu>
96
131
  </span>
97
132
  </MuiCardActions>
98
133
  )}
@@ -0,0 +1,85 @@
1
+ import { ReactElement, useCallback, useMemo } from 'react';
2
+
3
+ import { string, object } from 'yup';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { InputType } from '../../../Form/Inputs/models';
7
+ import { Form, FormProps } from '../../../Form';
8
+ import { FormVariant } from '../Form.models';
9
+ import { FormActions, FormActionsProps } from '../FormActions';
10
+
11
+ import { useStyles } from './DashboardForm.styles';
12
+ import {
13
+ labelMustBeAtLeast,
14
+ labelMustBeMost,
15
+ labelRequired
16
+ } from './translatedLabels';
17
+ import { DashboardResource } from './Dashboard.resource';
18
+
19
+ type DashboardFormProps = {
20
+ labels: DashboardFormLabels;
21
+ name: string;
22
+ onSubmit?: FormProps<DashboardResource>['submit'];
23
+ variant?: FormVariant;
24
+ } & Pick<FormActionsProps, 'onCancel'>;
25
+
26
+ type DashboardFormLabels = {
27
+ actions: FormActionsProps['labels'];
28
+ entity: Required<DashboardResource>;
29
+ };
30
+
31
+ const DashboardDuplicationForm = ({
32
+ variant = 'create',
33
+ labels,
34
+ onSubmit,
35
+ onCancel,
36
+ name
37
+ }: DashboardFormProps): ReactElement => {
38
+ const { classes } = useStyles();
39
+ const { t } = useTranslation();
40
+
41
+ const formProps = useMemo<FormProps<DashboardResource>>(
42
+ () => ({
43
+ initialValues: { name },
44
+ inputs: [
45
+ {
46
+ autoFocus: true,
47
+ fieldName: 'name',
48
+ group: 'main',
49
+ label: labels?.entity?.name,
50
+ required: true,
51
+ type: InputType.Text
52
+ }
53
+ ],
54
+ submit: (values, bag) => onSubmit?.(values, bag),
55
+ validationSchema: object({
56
+ name: string()
57
+ .label(labels?.entity?.name)
58
+ .min(3, ({ min, label }) => t(labelMustBeAtLeast, { label, min }))
59
+ .max(50, ({ max, label }) => t(labelMustBeMost, { label, max }))
60
+ .required(t(labelRequired) as string)
61
+ })
62
+ }),
63
+ [labels, onSubmit]
64
+ );
65
+
66
+ const Actions = useCallback(
67
+ () => (
68
+ <FormActions<DashboardResource>
69
+ enableSubmitWhenNotDirty
70
+ labels={labels?.actions}
71
+ variant={variant}
72
+ onCancel={onCancel}
73
+ />
74
+ ),
75
+ [labels, onCancel, variant]
76
+ );
77
+
78
+ return (
79
+ <div className={classes.dashboardForm}>
80
+ <Form<DashboardResource> {...formProps} Buttons={Actions} />
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export { DashboardDuplicationForm };
@@ -1,2 +1,3 @@
1
1
  export * from './DashboardForm';
2
2
  export * from './Dashboard.resource';
3
+ export * from './DashboardDuplicationForm';
@@ -8,6 +8,7 @@ import { FormVariant } from './Form.models';
8
8
  import { useStyles } from './Form.styles';
9
9
 
10
10
  export type FormActionsProps = {
11
+ enableSubmitWhenNotDirty?: boolean;
11
12
  labels: FormActionsLabels;
12
13
  onCancel: () => void;
13
14
  variant: FormVariant;
@@ -21,12 +22,16 @@ export type FormActionsLabels = {
21
22
  const FormActions = <TResource extends object>({
22
23
  labels,
23
24
  onCancel,
24
- variant
25
+ variant,
26
+ enableSubmitWhenNotDirty
25
27
  }: FormActionsProps): ReactElement => {
26
28
  const { classes } = useStyles();
27
29
  const { isSubmitting, dirty, isValid, submitForm } =
28
30
  useFormikContext<TResource>();
29
31
 
32
+ const isSubmitDisabled =
33
+ isSubmitting || (!dirty && !enableSubmitWhenNotDirty) || !isValid;
34
+
30
35
  return (
31
36
  <div className={classes.actions}>
32
37
  <Button
@@ -42,7 +47,7 @@ const FormActions = <TResource extends object>({
42
47
  <Button
43
48
  aria-label={labels.submit[variant]}
44
49
  data-testid="submit"
45
- disabled={isSubmitting || !dirty || !isValid}
50
+ disabled={isSubmitDisabled}
46
51
  size="medium"
47
52
  type="submit"
48
53
  variant="primary"