@centreon/ui 26.12.7 → 26.12.9
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,7 +1,7 @@
|
|
|
1
1
|
import { object } from 'yup';
|
|
2
2
|
|
|
3
3
|
import { Form, GroupDirection } from './Form';
|
|
4
|
-
import { InputType } from './Inputs/models';
|
|
4
|
+
import { type InputProps, InputType } from './Inputs/models';
|
|
5
5
|
import {
|
|
6
6
|
type BasicForm,
|
|
7
7
|
basicFormGroups,
|
|
@@ -149,3 +149,65 @@ export const segmentedButtons = (): JSX.Element => (
|
|
|
149
149
|
validationSchema={object()}
|
|
150
150
|
/>
|
|
151
151
|
);
|
|
152
|
+
|
|
153
|
+
const getSeparatedFields = (count: number): Array<InputProps> =>
|
|
154
|
+
Array.from({ length: count }, (_, index) => ({
|
|
155
|
+
fieldName: `field${index + 1}`,
|
|
156
|
+
group: '',
|
|
157
|
+
label: `Field ${index + 1}`,
|
|
158
|
+
type: InputType.Text
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
const getSeparatedGrid = ({
|
|
162
|
+
className,
|
|
163
|
+
count,
|
|
164
|
+
name
|
|
165
|
+
}: {
|
|
166
|
+
className: string;
|
|
167
|
+
count: number;
|
|
168
|
+
name: string;
|
|
169
|
+
}): InputProps => ({
|
|
170
|
+
fieldName: name,
|
|
171
|
+
grid: {
|
|
172
|
+
className,
|
|
173
|
+
columnSeparator: true,
|
|
174
|
+
columns: getSeparatedFields(count).map(({ group, ...field }) => ({
|
|
175
|
+
...field,
|
|
176
|
+
fieldName: `${name}-${field.fieldName}`
|
|
177
|
+
}))
|
|
178
|
+
},
|
|
179
|
+
group: '',
|
|
180
|
+
label: name,
|
|
181
|
+
type: InputType.Grid
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// The grids of this story show 1, 2 or 3 columns as the panel is resized (drag
|
|
185
|
+
// its bottom right corner): the lines only come between columns, on every row.
|
|
186
|
+
export const columnSeparators = (): JSX.Element => (
|
|
187
|
+
<div className="@container resize-x overflow-auto border border-dashed p-4">
|
|
188
|
+
<Form
|
|
189
|
+
initialValues={{}}
|
|
190
|
+
inputs={[
|
|
191
|
+
getSeparatedGrid({
|
|
192
|
+
className: 'grid-cols-1 gap-x-8 @[500px]:grid-cols-2',
|
|
193
|
+
count: 2,
|
|
194
|
+
name: 'two-columns'
|
|
195
|
+
}),
|
|
196
|
+
getSeparatedGrid({
|
|
197
|
+
className:
|
|
198
|
+
'grid-cols-1 gap-x-8 @[500px]:grid-cols-2 @[900px]:grid-cols-3',
|
|
199
|
+
count: 3,
|
|
200
|
+
name: 'three-columns'
|
|
201
|
+
}),
|
|
202
|
+
getSeparatedGrid({
|
|
203
|
+
className:
|
|
204
|
+
'grid-cols-1 gap-x-8 @[500px]:grid-cols-2 @[900px]:grid-cols-3',
|
|
205
|
+
count: 5,
|
|
206
|
+
name: 'wrapping-rows'
|
|
207
|
+
})
|
|
208
|
+
]}
|
|
209
|
+
submit={submit}
|
|
210
|
+
validationSchema={object()}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { rs } from '@rstest/core';
|
|
2
|
+
import { render } from '@test-setup/render';
|
|
3
|
+
import { screen } from '@testing-library/react';
|
|
4
|
+
import { object } from 'yup';
|
|
5
|
+
|
|
6
|
+
import { Form } from '../Form';
|
|
7
|
+
import { type InputProps, InputType } from './models';
|
|
8
|
+
|
|
9
|
+
const getField = (
|
|
10
|
+
name: string,
|
|
11
|
+
extra: Partial<InputProps> = {}
|
|
12
|
+
): Omit<InputProps, 'group'> => ({
|
|
13
|
+
fieldName: name,
|
|
14
|
+
label: name,
|
|
15
|
+
type: InputType.Text,
|
|
16
|
+
...extra
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
interface InitializeProps {
|
|
20
|
+
columnSeparator?: boolean;
|
|
21
|
+
// The tracks of the grid: what makes the number of columns follow the width.
|
|
22
|
+
columns: string;
|
|
23
|
+
fields: Array<Omit<InputProps, 'group'>>;
|
|
24
|
+
width: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialize = ({
|
|
28
|
+
columns,
|
|
29
|
+
columnSeparator,
|
|
30
|
+
fields,
|
|
31
|
+
width
|
|
32
|
+
}: InitializeProps): void => {
|
|
33
|
+
render(
|
|
34
|
+
<div style={{ padding: 40, width }}>
|
|
35
|
+
<Form
|
|
36
|
+
initialValues={{}}
|
|
37
|
+
inputs={[
|
|
38
|
+
{
|
|
39
|
+
fieldName: 'layout',
|
|
40
|
+
grid: {
|
|
41
|
+
columnSeparator,
|
|
42
|
+
columns: fields,
|
|
43
|
+
gridTemplateColumns: columns
|
|
44
|
+
},
|
|
45
|
+
group: '',
|
|
46
|
+
label: 'layout',
|
|
47
|
+
type: InputType.Grid
|
|
48
|
+
}
|
|
49
|
+
]}
|
|
50
|
+
submit={rs.fn()}
|
|
51
|
+
validationSchema={object()}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// The lines the grid draws, one for each of its items.
|
|
58
|
+
const getLines = (): Array<HTMLElement> =>
|
|
59
|
+
Array.from(document.querySelectorAll('[data-column-separator]'));
|
|
60
|
+
|
|
61
|
+
// A line is seen when what is painted at its place is the line itself: outside
|
|
62
|
+
// the grid, where it is clipped, there is something else.
|
|
63
|
+
const getVisibleLines = (): Array<boolean> =>
|
|
64
|
+
getLines().map((line) => {
|
|
65
|
+
const { left, top } = line.getBoundingClientRect();
|
|
66
|
+
|
|
67
|
+
return document.elementFromPoint(left + 0.5, top + 10) === line;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const threeFields = ['one', 'two', 'three'].map((name) => getField(name));
|
|
71
|
+
|
|
72
|
+
describe('Grid column separator', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
expect.hasAssertions();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('draws nothing unless asked to', async () => {
|
|
78
|
+
initialize({
|
|
79
|
+
columns: 'repeat(2, 1fr)',
|
|
80
|
+
fields: threeFields,
|
|
81
|
+
width: 800
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await screen.findByLabelText('one');
|
|
85
|
+
|
|
86
|
+
expect(document.querySelectorAll('[data-column-separator]')).toHaveLength(
|
|
87
|
+
0
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('draws a dashed line between two columns, not left of the first', async () => {
|
|
92
|
+
initialize({
|
|
93
|
+
columnSeparator: true,
|
|
94
|
+
columns: 'repeat(2, 1fr)',
|
|
95
|
+
fields: [getField('one'), getField('two')],
|
|
96
|
+
width: 800
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await screen.findByLabelText('one');
|
|
100
|
+
|
|
101
|
+
expect(getVisibleLines()).toEqual([false, true]);
|
|
102
|
+
|
|
103
|
+
const line = getComputedStyle(getLines()[1]);
|
|
104
|
+
|
|
105
|
+
expect(line.borderLeftStyle).toBe('dashed');
|
|
106
|
+
expect(line.borderLeftWidth).toBe('1px');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('draws nothing when the layout has a single column', async () => {
|
|
110
|
+
initialize({
|
|
111
|
+
columnSeparator: true,
|
|
112
|
+
columns: 'repeat(auto-fill, minmax(500px, 1fr))',
|
|
113
|
+
fields: [getField('one'), getField('two')],
|
|
114
|
+
width: 400
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await screen.findByLabelText('one');
|
|
118
|
+
|
|
119
|
+
expect(getVisibleLines()).toEqual([false, false]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('follows the number of columns the layout shows', async () => {
|
|
123
|
+
initialize({
|
|
124
|
+
columnSeparator: true,
|
|
125
|
+
columns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
|
126
|
+
fields: threeFields,
|
|
127
|
+
width: 1100
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await screen.findByLabelText('one');
|
|
131
|
+
|
|
132
|
+
expect(getVisibleLines()).toEqual([false, true, true]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('draws the lines of every row when the items wrap', async () => {
|
|
136
|
+
initialize({
|
|
137
|
+
columnSeparator: true,
|
|
138
|
+
columns: 'repeat(3, 1fr)',
|
|
139
|
+
fields: ['a', 'b', 'c', 'd', 'e'].map((name) => getField(name)),
|
|
140
|
+
width: 1100
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await screen.findByLabelText('a');
|
|
144
|
+
|
|
145
|
+
expect(getVisibleLines()).toEqual([false, true, true, false, true]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('spans the height of the row, not of the item', async () => {
|
|
149
|
+
initialize({
|
|
150
|
+
columnSeparator: true,
|
|
151
|
+
columns: 'repeat(2, 1fr)',
|
|
152
|
+
fields: [
|
|
153
|
+
getField('short'),
|
|
154
|
+
getField('tall', { text: { multilineRows: 6 } })
|
|
155
|
+
],
|
|
156
|
+
width: 800
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await screen.findByLabelText('short');
|
|
160
|
+
|
|
161
|
+
const [short, tall] = getLines().map(
|
|
162
|
+
(item) => item.getBoundingClientRect().height
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(tall).toBeGreaterThan(100);
|
|
166
|
+
expect(short).toBeCloseTo(tall, 0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('leaves no line for a hidden item and follows the items left', async () => {
|
|
170
|
+
initialize({
|
|
171
|
+
columnSeparator: true,
|
|
172
|
+
columns: 'repeat(2, 1fr)',
|
|
173
|
+
fields: [
|
|
174
|
+
getField('one'),
|
|
175
|
+
getField('hidden', { hideInput: () => true }),
|
|
176
|
+
getField('three')
|
|
177
|
+
],
|
|
178
|
+
width: 800
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await screen.findByLabelText('one');
|
|
182
|
+
|
|
183
|
+
expect(getLines()).toHaveLength(2);
|
|
184
|
+
expect(getVisibleLines()).toEqual([false, true]);
|
|
185
|
+
});
|
|
186
|
+
});
|
package/src/Form/Inputs/Grid.tsx
CHANGED
|
@@ -2,10 +2,36 @@ import { Box, Typography } from '@mui/material';
|
|
|
2
2
|
|
|
3
3
|
import { type FormikValues, useFormikContext } from 'formik';
|
|
4
4
|
import { isNotEmpty, isNotNil } from 'ramda';
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
5
6
|
|
|
6
7
|
import { getInput } from '.';
|
|
7
8
|
import type { InputPropsWithoutGroup } from './models';
|
|
8
9
|
|
|
10
|
+
// The line is drawn left of every item, in the middle of the gap, and the grid
|
|
11
|
+
// clips what falls out of it: the items of the first column lose theirs, so it
|
|
12
|
+
// never shows in a single column nor left of the first one, whatever the number
|
|
13
|
+
// of columns, without knowing it. The clip keeps 4px around the grid so that
|
|
14
|
+
// nothing the items draw outside their box is cut.
|
|
15
|
+
const columnGap = 'calc(var(--spacing) * 8)';
|
|
16
|
+
|
|
17
|
+
const columnSeparatorGridStyle = {
|
|
18
|
+
'--form-grid-column-gap': columnGap,
|
|
19
|
+
columnGap: 'var(--form-grid-column-gap)',
|
|
20
|
+
overflowClipMargin: '4px',
|
|
21
|
+
overflowX: 'clip'
|
|
22
|
+
} as CSSProperties;
|
|
23
|
+
|
|
24
|
+
// The dashed line of an item: half a gap to its left, over the height of its
|
|
25
|
+
// content.
|
|
26
|
+
const ColumnSeparator = (): JSX.Element => (
|
|
27
|
+
<span
|
|
28
|
+
aria-hidden
|
|
29
|
+
className="absolute inset-y-0 border-l border-dashed border-divider"
|
|
30
|
+
data-column-separator
|
|
31
|
+
style={{ left: 'calc(var(--form-grid-column-gap) / -2)' }}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
|
|
9
35
|
const Grid = ({
|
|
10
36
|
grid,
|
|
11
37
|
hideInput
|
|
@@ -17,6 +43,7 @@ const Grid = ({
|
|
|
17
43
|
}
|
|
18
44
|
|
|
19
45
|
const className = grid?.className || '';
|
|
46
|
+
const hasColumnSeparator = grid?.columnSeparator ?? false;
|
|
20
47
|
|
|
21
48
|
return (
|
|
22
49
|
<div
|
|
@@ -26,7 +53,8 @@ const Grid = ({
|
|
|
26
53
|
gridTemplateColumns: className
|
|
27
54
|
? grid?.gridTemplateColumns || undefined
|
|
28
55
|
: grid?.gridTemplateColumns ||
|
|
29
|
-
`repeat(${grid?.columns.length || 1}, 1fr)
|
|
56
|
+
`repeat(${grid?.columns.length || 1}, 1fr)`,
|
|
57
|
+
...(hasColumnSeparator && columnSeparatorGridStyle)
|
|
30
58
|
}}
|
|
31
59
|
>
|
|
32
60
|
{grid?.columns.map((field) => {
|
|
@@ -41,17 +69,37 @@ const Grid = ({
|
|
|
41
69
|
return null;
|
|
42
70
|
}
|
|
43
71
|
|
|
72
|
+
const title = field.additionalLabel && (
|
|
73
|
+
<Typography
|
|
74
|
+
className={`mb-1 text-base font-bold text-text-primary ${field?.additionalLabelClassName || ''}`}
|
|
75
|
+
variant="h6"
|
|
76
|
+
>
|
|
77
|
+
{field.additionalLabel}
|
|
78
|
+
</Typography>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!hasColumnSeparator) {
|
|
82
|
+
return (
|
|
83
|
+
<Box key={key as string} sx={{ width: '100%' }}>
|
|
84
|
+
{title}
|
|
85
|
+
<Input {...field} />
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The item stretches to the height of its row and its content fills
|
|
91
|
+
// what its title leaves, so the line spans the whole row.
|
|
44
92
|
return (
|
|
45
|
-
<Box
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
93
|
+
<Box
|
|
94
|
+
className="flex flex-col self-stretch"
|
|
95
|
+
key={key as string}
|
|
96
|
+
sx={{ width: '100%' }}
|
|
97
|
+
>
|
|
98
|
+
{title}
|
|
99
|
+
<div className="relative flex-1">
|
|
100
|
+
<ColumnSeparator />
|
|
101
|
+
<Input {...field} />
|
|
102
|
+
</div>
|
|
55
103
|
</Box>
|
|
56
104
|
);
|
|
57
105
|
})}
|
|
@@ -32,7 +32,9 @@ const SegmentedButtons = ({
|
|
|
32
32
|
return useMemoComponent({
|
|
33
33
|
Component: (
|
|
34
34
|
<FormGroup>
|
|
35
|
-
|
|
35
|
+
{!segmentedButtons?.hideLabel && (
|
|
36
|
+
<FormLabel className="font-bold">{t(label)}</FormLabel>
|
|
37
|
+
)}
|
|
36
38
|
{/* In a column narrower than the three minimum widths, the group scrolls
|
|
37
39
|
inside it rather than overflowing into what is next to it. */}
|
|
38
40
|
<div className="max-w-full self-start overflow-x-auto">
|
|
@@ -146,6 +146,10 @@ export interface InputProps {
|
|
|
146
146
|
grid?: {
|
|
147
147
|
alignItems?: string;
|
|
148
148
|
className?: string;
|
|
149
|
+
// A dashed vertical line in the gap between two columns, never left of
|
|
150
|
+
// the first one, whatever the number of columns the layout shows. To set
|
|
151
|
+
// on the grids that place groups of fields side by side.
|
|
152
|
+
columnSeparator?: boolean;
|
|
149
153
|
columns: Array<Omit<InputProps, 'group'>>;
|
|
150
154
|
gridTemplateColumns?: string;
|
|
151
155
|
};
|
|
@@ -171,6 +175,8 @@ export interface InputProps {
|
|
|
171
175
|
};
|
|
172
176
|
required?: boolean;
|
|
173
177
|
segmentedButtons?: {
|
|
178
|
+
// Leaves the label out of the display; it stays the group's accessible name.
|
|
179
|
+
hideLabel?: boolean;
|
|
174
180
|
options: Array<{
|
|
175
181
|
label: string;
|
|
176
182
|
value: string;
|