@centreon/ui 26.10.2 → 26.10.4
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 +1 -1
- package/src/Form/Form.stories.tsx +55 -0
- package/src/Form/Inputs/CheckboxGroup.tsx +1 -5
- package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +174 -0
- package/src/Form/Inputs/ExclusiveCheckboxGroup.tsx +96 -0
- package/src/Form/Inputs/index.tsx +5 -0
- package/src/Form/Inputs/models.ts +9 -1
- package/src/ThemeProvider/tailwindcss.css +10 -0
package/package.json
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { object } from 'yup';
|
|
2
|
+
|
|
1
3
|
import { Form, GroupDirection } from './Form';
|
|
4
|
+
import { InputType } from './Inputs/models';
|
|
2
5
|
import {
|
|
3
6
|
type BasicForm,
|
|
4
7
|
basicFormGroups,
|
|
@@ -55,3 +58,55 @@ export const basicFormWithHorizontalGroups = (): JSX.Element => (
|
|
|
55
58
|
groups={basicFormGroups.filter((group) => group.order !== 3)}
|
|
56
59
|
/>
|
|
57
60
|
);
|
|
61
|
+
|
|
62
|
+
export const exclusiveCheckboxGroup = (): JSX.Element => (
|
|
63
|
+
<Form
|
|
64
|
+
initialValues={{
|
|
65
|
+
hostNotificationOptions: ['Down', 'Recovery'],
|
|
66
|
+
serviceNotificationOptions: ['None']
|
|
67
|
+
}}
|
|
68
|
+
inputs={[
|
|
69
|
+
{
|
|
70
|
+
exclusiveCheckboxGroup: {
|
|
71
|
+
direction: 'horizontal',
|
|
72
|
+
exclusiveLabel: 'No notifications',
|
|
73
|
+
exclusiveOption: 'None',
|
|
74
|
+
options: [
|
|
75
|
+
'Down',
|
|
76
|
+
'Unreachable',
|
|
77
|
+
'Recovery',
|
|
78
|
+
'Flapping',
|
|
79
|
+
'Downtime Scheduled',
|
|
80
|
+
'None'
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
fieldName: 'hostNotificationOptions',
|
|
84
|
+
group: '',
|
|
85
|
+
label: 'Host notification options',
|
|
86
|
+
type: InputType.ExclusiveCheckboxGroup
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
exclusiveCheckboxGroup: {
|
|
90
|
+
direction: 'horizontal',
|
|
91
|
+
exclusiveLabel: 'No notifications',
|
|
92
|
+
exclusiveOption: 'None',
|
|
93
|
+
options: [
|
|
94
|
+
'Warning',
|
|
95
|
+
'Unknown',
|
|
96
|
+
'Critical',
|
|
97
|
+
'Recovery',
|
|
98
|
+
'Flapping',
|
|
99
|
+
'Downtime Scheduled',
|
|
100
|
+
'None'
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
fieldName: 'serviceNotificationOptions',
|
|
104
|
+
group: '',
|
|
105
|
+
label: 'Service notification options',
|
|
106
|
+
type: InputType.ExclusiveCheckboxGroup
|
|
107
|
+
}
|
|
108
|
+
]}
|
|
109
|
+
submit={submit}
|
|
110
|
+
validationSchema={object()}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
@@ -29,11 +29,7 @@ const CheckboxGroup = ({
|
|
|
29
29
|
if (!disabled && !hideCheckbox) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
...(element as unknown as Record<string, unknown>),
|
|
34
|
-
checked: false
|
|
35
|
-
}));
|
|
36
|
-
setFieldValue(fieldName, resetedValue);
|
|
32
|
+
setFieldValue(fieldName, []);
|
|
37
33
|
}, [disabled, hideCheckbox, fieldName, setFieldValue]);
|
|
38
34
|
|
|
39
35
|
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Typography } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { useFormikContext } from 'formik';
|
|
4
|
+
import { object } from 'yup';
|
|
5
|
+
|
|
6
|
+
import { Form } from '../Form';
|
|
7
|
+
import { InputType } from './models';
|
|
8
|
+
|
|
9
|
+
const options = ['Down', 'Unreachable', 'Recovery', 'None'];
|
|
10
|
+
|
|
11
|
+
const ValueDisplay = (): JSX.Element => {
|
|
12
|
+
const { values } = useFormikContext<{
|
|
13
|
+
notificationOptions: Array<string> | null;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Typography data-testid="value">
|
|
18
|
+
{JSON.stringify(values.notificationOptions)}
|
|
19
|
+
</Typography>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const initialize = ({
|
|
24
|
+
notificationOptions = [],
|
|
25
|
+
disabled = false
|
|
26
|
+
}: {
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
notificationOptions?: Array<string> | null;
|
|
29
|
+
} = {}): void => {
|
|
30
|
+
cy.mount({
|
|
31
|
+
Component: (
|
|
32
|
+
<Form
|
|
33
|
+
initialValues={{ notificationOptions }}
|
|
34
|
+
inputs={[
|
|
35
|
+
{
|
|
36
|
+
dataTestId: 'notification-options',
|
|
37
|
+
exclusiveCheckboxGroup: {
|
|
38
|
+
direction: 'horizontal',
|
|
39
|
+
exclusiveLabel: 'No notifications',
|
|
40
|
+
exclusiveOption: 'None',
|
|
41
|
+
options
|
|
42
|
+
},
|
|
43
|
+
fieldName: 'notificationOptions',
|
|
44
|
+
getDisabled: () => disabled,
|
|
45
|
+
group: '',
|
|
46
|
+
label: 'Notification options',
|
|
47
|
+
type: InputType.ExclusiveCheckboxGroup
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
custom: { Component: ValueDisplay },
|
|
51
|
+
fieldName: 'value',
|
|
52
|
+
group: '',
|
|
53
|
+
label: 'value',
|
|
54
|
+
type: InputType.Custom
|
|
55
|
+
}
|
|
56
|
+
]}
|
|
57
|
+
submit={cy.stub()}
|
|
58
|
+
validationSchema={object()}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const toggle = (): Cypress.Chainable => cy.findByLabelText('No notifications');
|
|
65
|
+
|
|
66
|
+
const expectValue = (value: Array<string> | null): void => {
|
|
67
|
+
cy.findByTestId('value').should('have.text', JSON.stringify(value));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe('Exclusive checkbox group', () => {
|
|
71
|
+
it('displays the toggle and every option except the exclusive one', () => {
|
|
72
|
+
initialize();
|
|
73
|
+
|
|
74
|
+
toggle().should('not.be.checked');
|
|
75
|
+
cy.findByLabelText('Down').should('exist');
|
|
76
|
+
cy.findByLabelText('Unreachable').should('exist');
|
|
77
|
+
cy.findByLabelText('Recovery').should('exist');
|
|
78
|
+
cy.findByLabelText('None').should('not.exist');
|
|
79
|
+
|
|
80
|
+
cy.makeSnapshot();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('keeps the inherited state when nothing is selected', () => {
|
|
84
|
+
initialize();
|
|
85
|
+
|
|
86
|
+
toggle().should('not.be.checked');
|
|
87
|
+
cy.findByLabelText('Down').should('not.be.checked').and('be.enabled');
|
|
88
|
+
expectValue([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('treats a null value as the inherited state', () => {
|
|
92
|
+
initialize({ notificationOptions: null });
|
|
93
|
+
|
|
94
|
+
toggle().should('not.be.checked');
|
|
95
|
+
cy.findByLabelText('Down').should('not.be.checked').and('be.enabled');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('displays the exclusive state from the initial value', () => {
|
|
99
|
+
initialize({ notificationOptions: ['None'] });
|
|
100
|
+
|
|
101
|
+
toggle().should('be.checked');
|
|
102
|
+
cy.findByLabelText('Down').should('not.be.checked').and('be.disabled');
|
|
103
|
+
expectValue(['None']);
|
|
104
|
+
|
|
105
|
+
cy.makeSnapshot();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('displays a selection from the initial value', () => {
|
|
109
|
+
initialize({ notificationOptions: ['Down', 'Recovery'] });
|
|
110
|
+
|
|
111
|
+
toggle().should('not.be.checked');
|
|
112
|
+
cy.findByLabelText('Down').should('be.checked');
|
|
113
|
+
cy.findByLabelText('Unreachable').should('not.be.checked');
|
|
114
|
+
cy.findByLabelText('Recovery').should('be.checked');
|
|
115
|
+
expectValue(['Down', 'Recovery']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('goes from the inherited state to a selection and back', () => {
|
|
119
|
+
initialize();
|
|
120
|
+
|
|
121
|
+
cy.findByLabelText('Down').click();
|
|
122
|
+
cy.findByLabelText('Recovery').click();
|
|
123
|
+
expectValue(['Down', 'Recovery']);
|
|
124
|
+
|
|
125
|
+
cy.findByLabelText('Down').click();
|
|
126
|
+
cy.findByLabelText('Recovery').click();
|
|
127
|
+
expectValue([]);
|
|
128
|
+
toggle().should('not.be.checked');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('goes from the inherited state to the exclusive state and back', () => {
|
|
132
|
+
initialize();
|
|
133
|
+
|
|
134
|
+
toggle().click();
|
|
135
|
+
toggle().should('be.checked');
|
|
136
|
+
expectValue(['None']);
|
|
137
|
+
|
|
138
|
+
toggle().click();
|
|
139
|
+
toggle().should('not.be.checked');
|
|
140
|
+
expectValue([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('clears the selection when the toggle is turned on and leaves it empty when turned off', () => {
|
|
144
|
+
initialize({ notificationOptions: ['Down', 'Recovery'] });
|
|
145
|
+
|
|
146
|
+
toggle().click();
|
|
147
|
+
expectValue(['None']);
|
|
148
|
+
cy.findByLabelText('Down').should('not.be.checked').and('be.disabled');
|
|
149
|
+
cy.findByLabelText('Recovery').should('not.be.checked').and('be.disabled');
|
|
150
|
+
|
|
151
|
+
toggle().click();
|
|
152
|
+
expectValue([]);
|
|
153
|
+
cy.findByLabelText('Down').should('not.be.checked').and('be.enabled');
|
|
154
|
+
|
|
155
|
+
cy.findByLabelText('Unreachable').click();
|
|
156
|
+
expectValue(['Unreachable']);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('prevents ticking a box while the toggle is on', () => {
|
|
160
|
+
initialize({ notificationOptions: ['None'] });
|
|
161
|
+
|
|
162
|
+
cy.findByLabelText('Down').click({ force: true });
|
|
163
|
+
cy.findByLabelText('Down').should('not.be.checked');
|
|
164
|
+
expectValue(['None']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('disables both the toggle and the checkboxes when the input is disabled', () => {
|
|
168
|
+
initialize({ disabled: true, notificationOptions: ['Down'] });
|
|
169
|
+
|
|
170
|
+
toggle().should('be.disabled');
|
|
171
|
+
cy.findByLabelText('Down').should('be.checked').and('be.disabled');
|
|
172
|
+
expectValue(['Down']);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Box } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { type FormikValues, useFormikContext } from 'formik';
|
|
4
|
+
import { equals, includes, path, reject, split } from 'ramda';
|
|
5
|
+
import type { ChangeEvent } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useMemoComponent } from '../..';
|
|
8
|
+
import { CheckboxGroup as CheckboxGroupComponent } from '../../Checkbox';
|
|
9
|
+
import {
|
|
10
|
+
type ChangeArgs,
|
|
11
|
+
type InputPropsWithoutGroup,
|
|
12
|
+
InputType
|
|
13
|
+
} from './models';
|
|
14
|
+
import Switch from './Switch';
|
|
15
|
+
|
|
16
|
+
const ExclusiveCheckboxGroup = ({
|
|
17
|
+
exclusiveCheckboxGroup,
|
|
18
|
+
fieldName,
|
|
19
|
+
getDisabled,
|
|
20
|
+
dataTestId
|
|
21
|
+
}: InputPropsWithoutGroup): JSX.Element => {
|
|
22
|
+
const { values, setFieldValue } = useFormikContext<FormikValues>();
|
|
23
|
+
|
|
24
|
+
const fieldNamePath = split('.', fieldName);
|
|
25
|
+
|
|
26
|
+
const value = (path(fieldNamePath, values) as Array<string> | null) ?? [];
|
|
27
|
+
|
|
28
|
+
const exclusiveOption = exclusiveCheckboxGroup?.exclusiveOption as string;
|
|
29
|
+
const options = reject(
|
|
30
|
+
equals(exclusiveOption),
|
|
31
|
+
exclusiveCheckboxGroup?.options ?? []
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const isExclusive = includes(exclusiveOption, value);
|
|
35
|
+
const disabled = getDisabled?.(values) || false;
|
|
36
|
+
|
|
37
|
+
const changeToggle = ({ value: checked }: ChangeArgs): void => {
|
|
38
|
+
setFieldValue(fieldName, checked ? [exclusiveOption] : []);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const changeCheckbox = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
42
|
+
if (disabled || isExclusive) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const option = event.target.id;
|
|
47
|
+
|
|
48
|
+
setFieldValue(
|
|
49
|
+
fieldName,
|
|
50
|
+
includes(option, value)
|
|
51
|
+
? reject(equals(option), value)
|
|
52
|
+
: [...value, option]
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const baseTestId = dataTestId || fieldName;
|
|
57
|
+
|
|
58
|
+
return useMemoComponent({
|
|
59
|
+
Component: (
|
|
60
|
+
<Box sx={{ alignItems: 'center', columnGap: 2, display: 'flex' }}>
|
|
61
|
+
<CheckboxGroupComponent
|
|
62
|
+
dataTestId={baseTestId}
|
|
63
|
+
direction={exclusiveCheckboxGroup?.direction}
|
|
64
|
+
disabled={disabled || isExclusive}
|
|
65
|
+
labelPlacement={exclusiveCheckboxGroup?.labelPlacement || 'end'}
|
|
66
|
+
onChange={changeCheckbox}
|
|
67
|
+
options={options}
|
|
68
|
+
values={isExclusive ? [] : value}
|
|
69
|
+
/>
|
|
70
|
+
<Switch
|
|
71
|
+
change={changeToggle}
|
|
72
|
+
dataTestId={`${baseTestId}-exclusive`}
|
|
73
|
+
fieldName={fieldName}
|
|
74
|
+
getDisabled={() => disabled}
|
|
75
|
+
label={exclusiveCheckboxGroup?.exclusiveLabel as string}
|
|
76
|
+
switchInput={{
|
|
77
|
+
getChecked: (fieldValue) =>
|
|
78
|
+
includes(exclusiveOption, (fieldValue as Array<string>) ?? [])
|
|
79
|
+
}}
|
|
80
|
+
type={InputType.Switch}
|
|
81
|
+
/>
|
|
82
|
+
</Box>
|
|
83
|
+
),
|
|
84
|
+
memoProps: [
|
|
85
|
+
value,
|
|
86
|
+
disabled,
|
|
87
|
+
options,
|
|
88
|
+
exclusiveOption,
|
|
89
|
+
exclusiveCheckboxGroup?.exclusiveLabel,
|
|
90
|
+
exclusiveCheckboxGroup?.direction,
|
|
91
|
+
exclusiveCheckboxGroup?.labelPlacement
|
|
92
|
+
]
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default ExclusiveCheckboxGroup;
|
|
@@ -35,6 +35,7 @@ import Checkbox from './Checkbox';
|
|
|
35
35
|
import CheckboxGroup from './CheckboxGroup';
|
|
36
36
|
import ConnectedAutocomplete from './ConnectedAutocomplete';
|
|
37
37
|
import Custom from './Custom';
|
|
38
|
+
import ExclusiveCheckboxGroup from './ExclusiveCheckboxGroup';
|
|
38
39
|
import FieldsTable from './FieldsTable/FieldsTable';
|
|
39
40
|
import File from './File';
|
|
40
41
|
import Grid from './Grid';
|
|
@@ -84,6 +85,10 @@ export const getInput = cond<
|
|
|
84
85
|
equals(InputType.CheckboxGroup) as (b: InputType) => boolean,
|
|
85
86
|
always(CheckboxGroup)
|
|
86
87
|
],
|
|
88
|
+
[
|
|
89
|
+
equals(InputType.ExclusiveCheckboxGroup) as (b: InputType) => boolean,
|
|
90
|
+
always(ExclusiveCheckboxGroup)
|
|
91
|
+
],
|
|
87
92
|
[equals(InputType.List) as (b: InputType) => boolean, always(List)],
|
|
88
93
|
[equals(InputType.File) as (b: InputType) => boolean, always(File)],
|
|
89
94
|
[equals(InputType.Text) as (b: InputType) => boolean, always(TextInput)],
|
|
@@ -50,7 +50,8 @@ export enum InputType {
|
|
|
50
50
|
CheckboxGroup = 12,
|
|
51
51
|
List = 13,
|
|
52
52
|
File = 14,
|
|
53
|
-
Divider = 15
|
|
53
|
+
Divider = 15,
|
|
54
|
+
ExclusiveCheckboxGroup = 16
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
interface FieldsTableGetRequiredProps {
|
|
@@ -98,6 +99,13 @@ export interface InputProps {
|
|
|
98
99
|
limitTags?: number;
|
|
99
100
|
decoder?: JsonDecoder.Decoder<unknown>;
|
|
100
101
|
};
|
|
102
|
+
exclusiveCheckboxGroup?: {
|
|
103
|
+
direction?: 'horizontal' | 'vertical';
|
|
104
|
+
exclusiveLabel: string;
|
|
105
|
+
exclusiveOption: string;
|
|
106
|
+
labelPlacement?: LabelPlacement;
|
|
107
|
+
options: Array<string>;
|
|
108
|
+
};
|
|
101
109
|
file?: {
|
|
102
110
|
multiple?: boolean;
|
|
103
111
|
accept?: string;
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
--color-background-tooltip: #434e58;
|
|
54
54
|
--color-background-widget: #f8f8f8;
|
|
55
55
|
|
|
56
|
+
/* Brand colors. Deliberately not overridden in the dark variant: they paint
|
|
57
|
+
brand surfaces that stay the same in both themes. */
|
|
58
|
+
--color-brand-navy: #131d5a;
|
|
59
|
+
--color-brand-navy-action: #cde7fc;
|
|
60
|
+
--color-brand-navy-action-text: #2e68aa;
|
|
61
|
+
|
|
56
62
|
/* Chip colors */
|
|
57
63
|
--color-chip-error: #ff6666;
|
|
58
64
|
--color-chip-info: #1588d1;
|
|
@@ -105,6 +111,9 @@
|
|
|
105
111
|
--color-secondary-light: #e5a5f0;
|
|
106
112
|
--color-secondary-dark: #ac28c1;
|
|
107
113
|
|
|
114
|
+
/* Section title color */
|
|
115
|
+
--color-section-title: #131d5a;
|
|
116
|
+
|
|
108
117
|
/* Status colors */
|
|
109
118
|
--color-status-background-error: #ff6e6e;
|
|
110
119
|
--color-status-background-none: --alpha(#2e68aa / 10%);
|
|
@@ -199,6 +208,7 @@
|
|
|
199
208
|
--color-primary-dark: #4974a5;
|
|
200
209
|
--color-primary-contrastText: #000;
|
|
201
210
|
--color-secondary-main: #7c1fa2;
|
|
211
|
+
--color-section-title: #ffffff;
|
|
202
212
|
--color-status-background-error: #ff4a4a;
|
|
203
213
|
--color-status-background-none: --alpha(#2e68aa / 10%);
|
|
204
214
|
--color-status-background-pending: #1ebeb3;
|