@centreon/ui 25.3.4 → 25.4.1
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/CollapsibleGroup.tsx +3 -3
- package/src/Form/Inputs/ConnectedAutocomplete.tsx +2 -0
- package/src/Form/Inputs/index.tsx +15 -11
- package/src/Form/Inputs/models.ts +3 -0
- package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +68 -14
- package/src/InputField/Select/Autocomplete/Connected/index.tsx +2 -1
- package/src/InputField/Select/Autocomplete/Multi/Listbox.tsx +78 -0
- package/src/InputField/Select/Autocomplete/Multi/Multi.styles.ts +26 -0
- package/src/InputField/Select/Autocomplete/Multi/Multi.tsx +124 -0
- package/src/InputField/Select/Autocomplete/Multi/index.tsx +1 -117
- package/src/InputField/translatedLabels.ts +4 -0
- package/src/Pagination/Pagination.cypress.spec.tsx +137 -0
- package/src/Pagination/Pagination.stories.tsx +46 -0
- package/src/Pagination/Pagination.styles.ts +56 -0
- package/src/Pagination/Pagination.tsx +146 -0
- package/src/Pagination/index.ts +3 -0
- package/src/Pagination/utils.ts +7 -0
- package/src/index.ts +1 -0
- package/src/queryParameters/url/index.ts +7 -2
package/package.json
CHANGED
|
@@ -3,8 +3,8 @@ import { useCallback, useState } from 'react';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { makeStyles } from 'tss-react/mui';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
|
|
6
|
+
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
|
7
|
+
|
|
8
8
|
import {
|
|
9
9
|
Box,
|
|
10
10
|
Collapse,
|
|
@@ -66,7 +66,7 @@ const CollapsibleGroup = ({
|
|
|
66
66
|
|
|
67
67
|
const containerClassName = className || '';
|
|
68
68
|
|
|
69
|
-
const CollapseIcon = isOpen ?
|
|
69
|
+
const CollapseIcon = isOpen ? ExpandLess : ExpandMore;
|
|
70
70
|
const ContainerComponent = useCallback(
|
|
71
71
|
({
|
|
72
72
|
children: containerComponentChildren
|
|
@@ -143,6 +143,8 @@ const ConnectedAutocomplete = ({
|
|
|
143
143
|
value={value ?? null}
|
|
144
144
|
onBlur={blur}
|
|
145
145
|
onChange={changeAutocomplete}
|
|
146
|
+
disableSelectAll={connectedAutocomplete?.disableSelectAll}
|
|
147
|
+
limitTags={connectedAutocomplete?.limitTags}
|
|
146
148
|
/>
|
|
147
149
|
),
|
|
148
150
|
memoProps: [
|
|
@@ -203,6 +203,8 @@ const Inputs = ({
|
|
|
203
203
|
? find(propEq(groupName, 'name'), groups)
|
|
204
204
|
: ({} as Group);
|
|
205
205
|
|
|
206
|
+
const hasGroupDivider = !groups[index]?.isDividerHidden;
|
|
207
|
+
|
|
206
208
|
const isFirstElement = areGroupsOpen || equals(index, 0);
|
|
207
209
|
|
|
208
210
|
return (
|
|
@@ -253,17 +255,19 @@ const Inputs = ({
|
|
|
253
255
|
</div>
|
|
254
256
|
</CollapsibleGroup>
|
|
255
257
|
</div>
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
258
|
+
{hasGroupDivider &&
|
|
259
|
+
hasGroupTitle &&
|
|
260
|
+
not(equals(lastGroup, groupName as string)) && (
|
|
261
|
+
<Divider
|
|
262
|
+
flexItem
|
|
263
|
+
className={classes.divider}
|
|
264
|
+
orientation={
|
|
265
|
+
equals(groupDirection, GroupDirection.Horizontal)
|
|
266
|
+
? 'vertical'
|
|
267
|
+
: 'horizontal'
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
267
271
|
</Fragment>
|
|
268
272
|
);
|
|
269
273
|
})}
|
|
@@ -59,6 +59,8 @@ export interface InputProps {
|
|
|
59
59
|
endpoint?: string;
|
|
60
60
|
filterKey?: string;
|
|
61
61
|
getRenderedOptionText?: (option) => string | JSX.Element;
|
|
62
|
+
disableSelectAll?: boolean;
|
|
63
|
+
limitTags?: number;
|
|
62
64
|
};
|
|
63
65
|
file?: {
|
|
64
66
|
multiple?: boolean;
|
|
@@ -133,4 +135,5 @@ export interface Group {
|
|
|
133
135
|
name: string;
|
|
134
136
|
order: number;
|
|
135
137
|
titleAttributes?: TypographyProps;
|
|
138
|
+
isDividerHidden?: boolean;
|
|
136
139
|
}
|
|
@@ -4,12 +4,40 @@ import {
|
|
|
4
4
|
TestQueryProvider
|
|
5
5
|
} from '@centreon/ui';
|
|
6
6
|
|
|
7
|
+
import i18next from 'i18next';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { initReactI18next } from 'react-i18next';
|
|
10
|
+
import { labelSelectAll, labelUnSelectAll } from '../../../../translatedLabels';
|
|
7
11
|
import { baseEndpoint, getEndpoint, label, placeholder } from './utils';
|
|
8
12
|
|
|
9
13
|
const optionOne = 'My Option 1';
|
|
10
14
|
|
|
15
|
+
const Component = () => {
|
|
16
|
+
const [values, setValues] = useState([]);
|
|
17
|
+
return (
|
|
18
|
+
<TestQueryProvider>
|
|
19
|
+
<div style={{ paddingTop: 20 }}>
|
|
20
|
+
<MultiConnectedAutocompleteField
|
|
21
|
+
field="host.name"
|
|
22
|
+
getEndpoint={getEndpoint}
|
|
23
|
+
label={label}
|
|
24
|
+
placeholder={placeholder}
|
|
25
|
+
value={values}
|
|
26
|
+
onChange={(_, item) => setValues(item)}
|
|
27
|
+
disableSelectAll={false}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</TestQueryProvider>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
11
34
|
describe('Multi connected autocomplete', () => {
|
|
12
35
|
beforeEach(() => {
|
|
36
|
+
i18next.use(initReactI18next).init({
|
|
37
|
+
lng: 'en',
|
|
38
|
+
resources: {}
|
|
39
|
+
});
|
|
40
|
+
|
|
13
41
|
cy.fixture('inputField/listOptions').then((optionsData) => {
|
|
14
42
|
cy.interceptAPIRequest({
|
|
15
43
|
alias: 'getListOptions',
|
|
@@ -35,18 +63,7 @@ describe('Multi connected autocomplete', () => {
|
|
|
35
63
|
});
|
|
36
64
|
|
|
37
65
|
cy.mount({
|
|
38
|
-
Component:
|
|
39
|
-
<TestQueryProvider>
|
|
40
|
-
<div style={{ paddingTop: 20 }}>
|
|
41
|
-
<MultiConnectedAutocompleteField
|
|
42
|
-
field="host.name"
|
|
43
|
-
getEndpoint={getEndpoint}
|
|
44
|
-
label={label}
|
|
45
|
-
placeholder={placeholder}
|
|
46
|
-
/>
|
|
47
|
-
</div>
|
|
48
|
-
</TestQueryProvider>
|
|
49
|
-
)
|
|
66
|
+
Component: <Component />
|
|
50
67
|
});
|
|
51
68
|
});
|
|
52
69
|
|
|
@@ -98,7 +115,7 @@ describe('Multi connected autocomplete', () => {
|
|
|
98
115
|
cy.waitForRequest('@getSearchedOption');
|
|
99
116
|
|
|
100
117
|
cy.fixture('inputField/searchedOption').then(() => {
|
|
101
|
-
cy.get('@listOptions').find('li').should('have.length',
|
|
118
|
+
cy.get('@listOptions').find('li').should('have.length', 6);
|
|
102
119
|
});
|
|
103
120
|
|
|
104
121
|
cy.get('[type="checkbox"]').eq(0).check();
|
|
@@ -115,7 +132,7 @@ describe('Multi connected autocomplete', () => {
|
|
|
115
132
|
cy.fixture('inputField/listOptions').then((optionsData) => {
|
|
116
133
|
cy.get('@listOptions')
|
|
117
134
|
.find('li')
|
|
118
|
-
.should('have.length', optionsData.result.length);
|
|
135
|
+
.should('have.length', optionsData.result.length + 1);
|
|
119
136
|
|
|
120
137
|
cy.get('@listOptions').within(() => {
|
|
121
138
|
optionsData.result.forEach((option) => {
|
|
@@ -124,4 +141,41 @@ describe('Multi connected autocomplete', () => {
|
|
|
124
141
|
});
|
|
125
142
|
});
|
|
126
143
|
});
|
|
144
|
+
|
|
145
|
+
it('checks all options when Select all button is clicked', () => {
|
|
146
|
+
cy.get('[data-testid="Multi Connected Autocomplete"]').as('input');
|
|
147
|
+
|
|
148
|
+
cy.get('@input').click();
|
|
149
|
+
|
|
150
|
+
cy.contains('5 element(s) found');
|
|
151
|
+
|
|
152
|
+
cy.waitForRequest('@getListOptions');
|
|
153
|
+
|
|
154
|
+
cy.contains(labelSelectAll).click();
|
|
155
|
+
|
|
156
|
+
cy.contains(labelUnSelectAll).should('be.visible');
|
|
157
|
+
|
|
158
|
+
cy.get('[data-testid="CancelIcon"]').should('have.length', 5);
|
|
159
|
+
|
|
160
|
+
cy.makeSnapshot('checks all options when Select all button is clicked');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('unchecks all options when unSelect all button is clicked', () => {
|
|
164
|
+
cy.get('[data-testid="Multi Connected Autocomplete"]').as('input');
|
|
165
|
+
|
|
166
|
+
cy.get('@input').click();
|
|
167
|
+
|
|
168
|
+
cy.contains('5 element(s) found');
|
|
169
|
+
|
|
170
|
+
cy.waitForRequest('@getListOptions');
|
|
171
|
+
|
|
172
|
+
cy.contains(labelSelectAll).click();
|
|
173
|
+
cy.contains(labelUnSelectAll).click();
|
|
174
|
+
|
|
175
|
+
cy.contains(labelSelectAll).should('be.visible');
|
|
176
|
+
|
|
177
|
+
cy.get('[data-testid="CancelIcon"]').should('have.length', 0);
|
|
178
|
+
|
|
179
|
+
cy.makeSnapshot('unchecks all options when unSelect all button is clicked');
|
|
180
|
+
});
|
|
127
181
|
});
|
|
@@ -88,7 +88,7 @@ const ConnectedAutocompleteField = (
|
|
|
88
88
|
|
|
89
89
|
const theme = useTheme();
|
|
90
90
|
|
|
91
|
-
const { fetchQuery, isFetching, prefetchNextPage } = useFetchQuery<
|
|
91
|
+
const { fetchQuery, isFetching, prefetchNextPage, data } = useFetchQuery<
|
|
92
92
|
ListingModel<TData>
|
|
93
93
|
>({
|
|
94
94
|
baseEndpoint,
|
|
@@ -322,6 +322,7 @@ const ConnectedAutocompleteField = (
|
|
|
322
322
|
|
|
323
323
|
return (
|
|
324
324
|
<AutocompleteField
|
|
325
|
+
total={data?.meta.total}
|
|
325
326
|
filterOptions={(opt): SelectEntry => opt}
|
|
326
327
|
loading={isFetching}
|
|
327
328
|
options={
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ListSubheader, Typography } from '@mui/material';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '../../../../components/Button';
|
|
4
|
+
import { useListboxStyles } from './Multi.styles';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
labelElementsFound,
|
|
8
|
+
labelSelectAll,
|
|
9
|
+
labelUnSelectAll
|
|
10
|
+
} from '../../../translatedLabels';
|
|
11
|
+
|
|
12
|
+
const CustomListbox = ({
|
|
13
|
+
children,
|
|
14
|
+
label,
|
|
15
|
+
labelTotal,
|
|
16
|
+
handleSelectAllToggle,
|
|
17
|
+
...props
|
|
18
|
+
}) => {
|
|
19
|
+
const { classes } = useListboxStyles();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ul {...props}>
|
|
23
|
+
<ListSubheader sx={{ padding: 0 }}>
|
|
24
|
+
<div className={classes.lisSubHeader}>
|
|
25
|
+
<Typography variant="body2">{labelTotal}</Typography>
|
|
26
|
+
<Button variant="ghost" size="small" onClick={handleSelectAllToggle}>
|
|
27
|
+
{label}
|
|
28
|
+
</Button>
|
|
29
|
+
</div>
|
|
30
|
+
</ListSubheader>
|
|
31
|
+
<div className={classes.dropdown}>{children}</div>
|
|
32
|
+
</ul>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ListboxComponent = ({
|
|
37
|
+
disableSelectAll,
|
|
38
|
+
options,
|
|
39
|
+
isOptionSelected,
|
|
40
|
+
onChange,
|
|
41
|
+
total
|
|
42
|
+
}) => {
|
|
43
|
+
if (disableSelectAll) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (listboxProps): JSX.Element | undefined => {
|
|
48
|
+
const { t } = useTranslation();
|
|
49
|
+
|
|
50
|
+
const allSelected =
|
|
51
|
+
options.length > 0 && options.every((opt) => isOptionSelected(opt));
|
|
52
|
+
|
|
53
|
+
const handleSelectAllToggle = (): void => {
|
|
54
|
+
const syntheticEvent = {} as React.SyntheticEvent;
|
|
55
|
+
|
|
56
|
+
if (allSelected) {
|
|
57
|
+
onChange?.(syntheticEvent, [], 'selectOption');
|
|
58
|
+
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onChange?.(syntheticEvent, options, 'selectOption');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<CustomListbox
|
|
67
|
+
{...listboxProps}
|
|
68
|
+
label={t(allSelected ? labelUnSelectAll : labelSelectAll)}
|
|
69
|
+
handleSelectAllToggle={handleSelectAllToggle}
|
|
70
|
+
labelTotal={t(labelElementsFound, {
|
|
71
|
+
total: total || options.length
|
|
72
|
+
})}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default ListboxComponent;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { makeStyles } from 'tss-react/mui';
|
|
2
|
+
|
|
3
|
+
export const useStyles = makeStyles()((theme) => ({
|
|
4
|
+
deleteIcon: {
|
|
5
|
+
height: theme.spacing(1.5),
|
|
6
|
+
width: theme.spacing(1.5)
|
|
7
|
+
},
|
|
8
|
+
tag: {
|
|
9
|
+
fontSize: theme.typography.caption.fontSize
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
export const useListboxStyles = makeStyles()((theme) => ({
|
|
14
|
+
lisSubHeader: {
|
|
15
|
+
width: '100%',
|
|
16
|
+
background: theme.palette.background.default,
|
|
17
|
+
padding: theme.spacing(0.5, 1, 0.5, 1.5),
|
|
18
|
+
display: 'flex',
|
|
19
|
+
justifyContent: 'space-between',
|
|
20
|
+
alignItems: 'center'
|
|
21
|
+
},
|
|
22
|
+
dropdown: {
|
|
23
|
+
width: '100%',
|
|
24
|
+
background: theme.palette.background.paper
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { compose, includes, map, prop, reject, sortBy, toLower } from 'ramda';
|
|
2
|
+
import { JSX } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Chip, ChipProps, Tooltip } from '@mui/material';
|
|
5
|
+
import { UseAutocompleteProps } from '@mui/material/useAutocomplete';
|
|
6
|
+
|
|
7
|
+
import Autocomplete, { Props as AutocompleteProps } from '..';
|
|
8
|
+
import { SelectEntry } from '../..';
|
|
9
|
+
import Option from '../../Option';
|
|
10
|
+
import ListboxComponent from './Listbox';
|
|
11
|
+
import { useStyles } from './Multi.styles';
|
|
12
|
+
|
|
13
|
+
type Multiple = boolean;
|
|
14
|
+
type DisableClearable = boolean;
|
|
15
|
+
type FreeSolo = boolean;
|
|
16
|
+
|
|
17
|
+
export interface Props
|
|
18
|
+
extends Omit<AutocompleteProps, 'renderTags' | 'renderOption' | 'multiple'>,
|
|
19
|
+
Omit<
|
|
20
|
+
UseAutocompleteProps<SelectEntry, Multiple, DisableClearable, FreeSolo>,
|
|
21
|
+
'multiple'
|
|
22
|
+
> {
|
|
23
|
+
chipProps?: ChipProps;
|
|
24
|
+
disableSortedOptions?: boolean;
|
|
25
|
+
disableSelectAll?: boolean;
|
|
26
|
+
getOptionTooltipLabel?: (option) => string;
|
|
27
|
+
getTagLabel?: (option) => string;
|
|
28
|
+
optionProperty?: string;
|
|
29
|
+
customRenderTags?: (tags: React.ReactNode) => React.ReactNode;
|
|
30
|
+
total?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MultiAutocompleteField = ({
|
|
34
|
+
value,
|
|
35
|
+
options,
|
|
36
|
+
disableSortedOptions = false,
|
|
37
|
+
disableSelectAll = true,
|
|
38
|
+
optionProperty = 'name',
|
|
39
|
+
getOptionLabel = (option): string => option.name,
|
|
40
|
+
getTagLabel = (option): string => option[optionProperty],
|
|
41
|
+
getOptionTooltipLabel,
|
|
42
|
+
chipProps,
|
|
43
|
+
customRenderTags,
|
|
44
|
+
onChange,
|
|
45
|
+
total,
|
|
46
|
+
...props
|
|
47
|
+
}: Props): JSX.Element => {
|
|
48
|
+
const { classes } = useStyles();
|
|
49
|
+
|
|
50
|
+
const renderTags = (renderedValue, getTagProps): Array<JSX.Element> =>
|
|
51
|
+
renderedValue.map((option, index) => {
|
|
52
|
+
return (
|
|
53
|
+
<Tooltip
|
|
54
|
+
key={option.id}
|
|
55
|
+
placement="top"
|
|
56
|
+
title={getOptionTooltipLabel?.(option)}
|
|
57
|
+
>
|
|
58
|
+
<Chip
|
|
59
|
+
classes={{
|
|
60
|
+
deleteIcon: classes.deleteIcon,
|
|
61
|
+
root: classes.tag
|
|
62
|
+
}}
|
|
63
|
+
data-testid={`tag-option-chip-${option.id}`}
|
|
64
|
+
label={getTagLabel(option)}
|
|
65
|
+
size="medium"
|
|
66
|
+
{...getTagProps({ index })}
|
|
67
|
+
{...chipProps}
|
|
68
|
+
onDelete={(event) => chipProps?.onDelete?.(event, option)}
|
|
69
|
+
/>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const getLimitTagsText = (more): JSX.Element => <Option>{`+${more}`}</Option>;
|
|
75
|
+
|
|
76
|
+
const values = (value as Array<SelectEntry>) || [];
|
|
77
|
+
|
|
78
|
+
const isOptionSelected = ({ id }): boolean => {
|
|
79
|
+
const valueIds = map(prop('id'), values);
|
|
80
|
+
|
|
81
|
+
return includes(id, valueIds);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const sortByName = sortBy(compose(toLower, prop(optionProperty)));
|
|
85
|
+
|
|
86
|
+
const autocompleteOptions = disableSortedOptions
|
|
87
|
+
? options
|
|
88
|
+
: sortByName([...values, ...reject(isOptionSelected, options)]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Autocomplete
|
|
92
|
+
disableCloseOnSelect
|
|
93
|
+
displayOptionThumbnail
|
|
94
|
+
multiple
|
|
95
|
+
getLimitTagsText={getLimitTagsText}
|
|
96
|
+
options={autocompleteOptions}
|
|
97
|
+
renderOption={(renderProps, option, { selected }): JSX.Element => (
|
|
98
|
+
<li
|
|
99
|
+
key={option.id}
|
|
100
|
+
{...(renderProps as React.HTMLAttributes<HTMLLIElement>)}
|
|
101
|
+
>
|
|
102
|
+
<Option checkboxSelected={selected}>{getOptionLabel(option)}</Option>
|
|
103
|
+
</li>
|
|
104
|
+
)}
|
|
105
|
+
value={values}
|
|
106
|
+
renderTags={(renderedValue, getTagProps): React.ReactNode =>
|
|
107
|
+
customRenderTags
|
|
108
|
+
? customRenderTags(renderTags(renderedValue, getTagProps))
|
|
109
|
+
: renderTags(renderedValue, getTagProps)
|
|
110
|
+
}
|
|
111
|
+
ListboxComponent={ListboxComponent({
|
|
112
|
+
total,
|
|
113
|
+
onChange,
|
|
114
|
+
isOptionSelected,
|
|
115
|
+
disableSelectAll,
|
|
116
|
+
options
|
|
117
|
+
})}
|
|
118
|
+
onChange={onChange}
|
|
119
|
+
{...props}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default MultiAutocompleteField;
|
|
@@ -1,119 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { makeStyles } from 'tss-react/mui';
|
|
3
|
-
|
|
4
|
-
import { Chip, ChipProps, Tooltip } from '@mui/material';
|
|
5
|
-
import { UseAutocompleteProps } from '@mui/material/useAutocomplete';
|
|
6
|
-
|
|
7
|
-
import Autocomplete, { Props as AutocompleteProps } from '..';
|
|
8
|
-
import { SelectEntry } from '../..';
|
|
9
|
-
import Option from '../../Option';
|
|
10
|
-
|
|
11
|
-
const useStyles = makeStyles()((theme) => ({
|
|
12
|
-
deleteIcon: {
|
|
13
|
-
height: theme.spacing(1.5),
|
|
14
|
-
width: theme.spacing(1.5)
|
|
15
|
-
},
|
|
16
|
-
tag: {
|
|
17
|
-
fontSize: theme.typography.caption.fontSize
|
|
18
|
-
}
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
type Multiple = boolean;
|
|
22
|
-
type DisableClearable = boolean;
|
|
23
|
-
type FreeSolo = boolean;
|
|
24
|
-
|
|
25
|
-
export interface Props
|
|
26
|
-
extends Omit<AutocompleteProps, 'renderTags' | 'renderOption' | 'multiple'>,
|
|
27
|
-
Omit<
|
|
28
|
-
UseAutocompleteProps<SelectEntry, Multiple, DisableClearable, FreeSolo>,
|
|
29
|
-
'multiple'
|
|
30
|
-
> {
|
|
31
|
-
chipProps?: ChipProps;
|
|
32
|
-
disableSortedOptions?: boolean;
|
|
33
|
-
getOptionTooltipLabel?: (option) => string;
|
|
34
|
-
getTagLabel?: (option) => string;
|
|
35
|
-
optionProperty?: string;
|
|
36
|
-
customRenderTags?: (tags: React.ReactNode) => React.ReactNode;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const MultiAutocompleteField = ({
|
|
40
|
-
value,
|
|
41
|
-
options,
|
|
42
|
-
disableSortedOptions = false,
|
|
43
|
-
optionProperty = 'name',
|
|
44
|
-
getOptionLabel = (option): string => option.name,
|
|
45
|
-
getTagLabel = (option): string => option[optionProperty],
|
|
46
|
-
getOptionTooltipLabel,
|
|
47
|
-
chipProps,
|
|
48
|
-
customRenderTags,
|
|
49
|
-
...props
|
|
50
|
-
}: Props): JSX.Element => {
|
|
51
|
-
const { classes } = useStyles();
|
|
52
|
-
|
|
53
|
-
const renderTags = (renderedValue, getTagProps): Array<JSX.Element> =>
|
|
54
|
-
renderedValue.map((option, index) => {
|
|
55
|
-
return (
|
|
56
|
-
<Tooltip
|
|
57
|
-
key={option.id}
|
|
58
|
-
placement="top"
|
|
59
|
-
title={getOptionTooltipLabel?.(option)}
|
|
60
|
-
>
|
|
61
|
-
<Chip
|
|
62
|
-
classes={{
|
|
63
|
-
deleteIcon: classes.deleteIcon,
|
|
64
|
-
root: classes.tag
|
|
65
|
-
}}
|
|
66
|
-
data-testid={`tag-option-chip-${option.id}`}
|
|
67
|
-
label={getTagLabel(option)}
|
|
68
|
-
size="medium"
|
|
69
|
-
{...getTagProps({ index })}
|
|
70
|
-
{...chipProps}
|
|
71
|
-
onDelete={(event) => chipProps?.onDelete?.(event, option)}
|
|
72
|
-
/>
|
|
73
|
-
</Tooltip>
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const getLimitTagsText = (more): JSX.Element => <Option>{`+${more}`}</Option>;
|
|
78
|
-
|
|
79
|
-
const values = (value as Array<SelectEntry>) || [];
|
|
80
|
-
|
|
81
|
-
const isOptionSelected = ({ id }): boolean => {
|
|
82
|
-
const valueIds = map(prop('id'), values);
|
|
83
|
-
|
|
84
|
-
return includes(id, valueIds);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const sortByName = sortBy(compose(toLower, prop(optionProperty)));
|
|
88
|
-
|
|
89
|
-
const autocompleteOptions = disableSortedOptions
|
|
90
|
-
? options
|
|
91
|
-
: sortByName([...values, ...reject(isOptionSelected, options)]);
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<Autocomplete
|
|
95
|
-
disableCloseOnSelect
|
|
96
|
-
displayOptionThumbnail
|
|
97
|
-
multiple
|
|
98
|
-
getLimitTagsText={getLimitTagsText}
|
|
99
|
-
options={autocompleteOptions}
|
|
100
|
-
renderOption={(renderProps, option, { selected }): JSX.Element => (
|
|
101
|
-
<li
|
|
102
|
-
key={option.id}
|
|
103
|
-
{...(renderProps as React.HTMLAttributes<HTMLLIElement>)}
|
|
104
|
-
>
|
|
105
|
-
<Option checkboxSelected={selected}>{getOptionLabel(option)}</Option>
|
|
106
|
-
</li>
|
|
107
|
-
)}
|
|
108
|
-
renderTags={(renderedValue, getTagProps): React.ReactNode =>
|
|
109
|
-
customRenderTags
|
|
110
|
-
? customRenderTags(renderTags(renderedValue, getTagProps))
|
|
111
|
-
: renderTags(renderedValue, getTagProps)
|
|
112
|
-
}
|
|
113
|
-
value={value}
|
|
114
|
-
{...props}
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
117
|
-
};
|
|
1
|
+
import MultiAutocompleteField from './Multi';
|
|
118
2
|
|
|
119
3
|
export default MultiAutocompleteField;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export const searchLabel = 'Search';
|
|
2
2
|
export const labelOpen = 'Open';
|
|
3
3
|
export const labelClear = 'Clear';
|
|
4
|
+
|
|
5
|
+
export const labelSelectAll = 'Select all';
|
|
6
|
+
export const labelUnSelectAll = 'Unselect all';
|
|
7
|
+
export const labelElementsFound = '{{total}} element(s) found';
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { labelNextPage, labelPreviousPage } from '../Listing/translatedLabels';
|
|
2
|
+
import TestQueryProvider from '../api/TestQueryProvider';
|
|
3
|
+
import { Method } from '../api/useMutationQuery';
|
|
4
|
+
import Pagination from './Pagination';
|
|
5
|
+
import { generateItems } from './utils';
|
|
6
|
+
|
|
7
|
+
const defaultTotalItems = 25;
|
|
8
|
+
const itemsPerPage = 6;
|
|
9
|
+
const totalPages = Math.ceil(defaultTotalItems / itemsPerPage);
|
|
10
|
+
|
|
11
|
+
const initialize = ({
|
|
12
|
+
total = defaultTotalItems,
|
|
13
|
+
currentPage = 1
|
|
14
|
+
}: { total?: number; currentPage?: number }) => {
|
|
15
|
+
cy.interceptAPIRequest({
|
|
16
|
+
alias: 'list',
|
|
17
|
+
method: Method.GET,
|
|
18
|
+
path: '**/listing**',
|
|
19
|
+
response: {
|
|
20
|
+
result: generateItems(itemsPerPage),
|
|
21
|
+
meta: {
|
|
22
|
+
page: currentPage,
|
|
23
|
+
total,
|
|
24
|
+
limit: itemsPerPage
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cy.mount({
|
|
30
|
+
Component: (
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
width: '100%',
|
|
34
|
+
height: '100vh',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
alignItems: 'center'
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
height: '176px',
|
|
43
|
+
boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.2)'
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<TestQueryProvider>
|
|
47
|
+
<Pagination
|
|
48
|
+
api={{ baseEndpoint: '/test/listing', queryKey: ['test'] }}
|
|
49
|
+
/>
|
|
50
|
+
</TestQueryProvider>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('Pagination Component', () => {
|
|
58
|
+
it('render with correct initial state', () => {
|
|
59
|
+
initialize({});
|
|
60
|
+
cy.waitForRequest('@list');
|
|
61
|
+
|
|
62
|
+
cy.findByTestId(labelPreviousPage).should('be.disabled');
|
|
63
|
+
|
|
64
|
+
cy.findByTestId(labelNextPage).should('not.be.disabled');
|
|
65
|
+
|
|
66
|
+
cy.contains(`Page 1/${totalPages}`);
|
|
67
|
+
|
|
68
|
+
cy.makeSnapshot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('hides pagination controls when only one page exists', () => {
|
|
72
|
+
initialize({ total: itemsPerPage });
|
|
73
|
+
cy.waitForRequest('@list');
|
|
74
|
+
|
|
75
|
+
cy.findByTestId(labelPreviousPage).should('not.exist');
|
|
76
|
+
cy.findByTestId(labelNextPage).should('not.exist');
|
|
77
|
+
cy.contains(/Page \d+\/\d+/).should('not.exist');
|
|
78
|
+
|
|
79
|
+
cy.makeSnapshot();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('navigates forward through pages correctly', () => {
|
|
83
|
+
initialize({});
|
|
84
|
+
cy.waitForRequest('@list');
|
|
85
|
+
|
|
86
|
+
cy.contains(`Page 1/${totalPages}`);
|
|
87
|
+
|
|
88
|
+
Array.from({ length: totalPages - 1 }).forEach((_, index) => {
|
|
89
|
+
cy.findByTestId(labelNextPage).click();
|
|
90
|
+
cy.waitForRequest('@list');
|
|
91
|
+
|
|
92
|
+
cy.contains(`Page ${index + 2}/${totalPages}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
cy.findByTestId(labelNextPage).should('be.disabled');
|
|
96
|
+
|
|
97
|
+
cy.makeSnapshot();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('navigates backward through pages correctly', () => {
|
|
101
|
+
initialize({});
|
|
102
|
+
|
|
103
|
+
cy.waitForRequest('@list');
|
|
104
|
+
|
|
105
|
+
Array.from({ length: totalPages - 1 }).forEach(() => {
|
|
106
|
+
cy.findByTestId(labelNextPage).click();
|
|
107
|
+
|
|
108
|
+
cy.waitForRequest('@list');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
Array.from({ length: totalPages - 1 }).forEach((_, index) => {
|
|
112
|
+
cy.findByTestId(labelPreviousPage).click();
|
|
113
|
+
cy.waitForRequest('@list');
|
|
114
|
+
|
|
115
|
+
cy.contains(`Page ${totalPages - index - 1}/${totalPages}`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
cy.findByTestId(labelPreviousPage).should('be.disabled');
|
|
119
|
+
|
|
120
|
+
cy.makeSnapshot();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('enables both buttons when on middle page', () => {
|
|
124
|
+
initialize({});
|
|
125
|
+
|
|
126
|
+
cy.waitForRequest('@list');
|
|
127
|
+
|
|
128
|
+
cy.findByTestId(labelNextPage).click();
|
|
129
|
+
|
|
130
|
+
cy.waitForRequest('@list');
|
|
131
|
+
|
|
132
|
+
cy.findByTestId(labelPreviousPage).should('not.be.disabled');
|
|
133
|
+
cy.findByTestId(labelNextPage).should('not.be.disabled');
|
|
134
|
+
|
|
135
|
+
cy.makeSnapshot();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { http, HttpResponse } from 'msw';
|
|
3
|
+
import Pagination from '.';
|
|
4
|
+
import { generateItems } from './utils';
|
|
5
|
+
|
|
6
|
+
const mockedListing = {
|
|
7
|
+
result: generateItems(6),
|
|
8
|
+
meta: {
|
|
9
|
+
page: 1,
|
|
10
|
+
total: 35,
|
|
11
|
+
limit: 6
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Pagination> = {
|
|
16
|
+
args: {},
|
|
17
|
+
component: Pagination,
|
|
18
|
+
parameters: {
|
|
19
|
+
msw: {
|
|
20
|
+
handlers: [
|
|
21
|
+
http.get('**/listing**', () => {
|
|
22
|
+
return HttpResponse.json(mockedListing);
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
render: (args) => {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
width: '240px',
|
|
32
|
+
background: '#EDEDED'
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<Pagination {...args} />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof Pagination>;
|
|
43
|
+
|
|
44
|
+
export const Default: Story = {
|
|
45
|
+
args: { api: { baseEndpoint: '/test/listing', queryKey: ['pagination'] } }
|
|
46
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { makeStyles } from 'tss-react/mui';
|
|
2
|
+
|
|
3
|
+
export const useStyles = makeStyles()((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
height: theme.spacing(22),
|
|
6
|
+
width: theme.spacing(30),
|
|
7
|
+
padding: theme.spacing(1),
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
justifyContent: 'space-between',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
gap: theme.spacing(2)
|
|
13
|
+
},
|
|
14
|
+
notFound: {
|
|
15
|
+
height: theme.spacing(10),
|
|
16
|
+
width: theme.spacing(30),
|
|
17
|
+
padding: theme.spacing(1)
|
|
18
|
+
},
|
|
19
|
+
body: {
|
|
20
|
+
width: '100%',
|
|
21
|
+
height: '100%',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
justifyContent: 'space-between',
|
|
24
|
+
gap: theme.spacing(1)
|
|
25
|
+
},
|
|
26
|
+
content: {
|
|
27
|
+
width: '100%',
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
gap: theme.spacing(0.5)
|
|
31
|
+
},
|
|
32
|
+
page: {
|
|
33
|
+
fontWeight: theme.typography.fontWeightMedium
|
|
34
|
+
},
|
|
35
|
+
arrowContainer: {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
justifyContent: 'space-between',
|
|
38
|
+
alignItems: 'center'
|
|
39
|
+
},
|
|
40
|
+
icon: {
|
|
41
|
+
color: theme.palette.text.primary
|
|
42
|
+
},
|
|
43
|
+
arrow: {
|
|
44
|
+
fontSize: theme.spacing(2)
|
|
45
|
+
},
|
|
46
|
+
item: {
|
|
47
|
+
color: 'inherit',
|
|
48
|
+
textDecoration: 'none'
|
|
49
|
+
},
|
|
50
|
+
link: {
|
|
51
|
+
'&:hover': {
|
|
52
|
+
cursor: 'pointer',
|
|
53
|
+
color: theme.palette.primary.main
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { CircularProgress, Link, Typography } from '@mui/material';
|
|
2
|
+
import { equals, isEmpty, isNil } from 'ramda';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import ArrowBackIcon from '@mui/icons-material/ArrowBackIosNew';
|
|
6
|
+
import ArrowForwardIcon from '@mui/icons-material/ArrowForwardIos';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import IconButton from '../Button/Icon';
|
|
9
|
+
import {
|
|
10
|
+
labelNextPage,
|
|
11
|
+
labelNoResultFound,
|
|
12
|
+
labelPreviousPage
|
|
13
|
+
} from '../Listing/translatedLabels';
|
|
14
|
+
import buildListingEndpoint from '../api/buildListingEndpoint';
|
|
15
|
+
import { Listing } from '../api/models';
|
|
16
|
+
import useFetchQuery from '../api/useFetchQuery';
|
|
17
|
+
import { truncate } from '../utils';
|
|
18
|
+
import { useStyles } from './Pagination.styles';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
api: {
|
|
22
|
+
baseEndpoint: string;
|
|
23
|
+
queryKey: Array<string>;
|
|
24
|
+
searchConditions?;
|
|
25
|
+
};
|
|
26
|
+
labelHasNoElements?: string;
|
|
27
|
+
onItemClick?: ({ id }: { id: number }) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const limit = 6;
|
|
31
|
+
|
|
32
|
+
const Pagination = ({
|
|
33
|
+
api: { baseEndpoint, queryKey, searchConditions },
|
|
34
|
+
labelHasNoElements = labelNoResultFound,
|
|
35
|
+
onItemClick
|
|
36
|
+
}: Props) => {
|
|
37
|
+
const { t } = useTranslation();
|
|
38
|
+
const { cx, classes } = useStyles();
|
|
39
|
+
const [page, setPage] = useState(1);
|
|
40
|
+
|
|
41
|
+
const { data, isLoading } = useFetchQuery<
|
|
42
|
+
Listing<{ id: number; name: string }>
|
|
43
|
+
>({
|
|
44
|
+
getEndpoint: (parameters): string =>
|
|
45
|
+
buildListingEndpoint({
|
|
46
|
+
baseEndpoint: baseEndpoint,
|
|
47
|
+
parameters: {
|
|
48
|
+
...parameters,
|
|
49
|
+
page,
|
|
50
|
+
limit,
|
|
51
|
+
...(searchConditions
|
|
52
|
+
? {
|
|
53
|
+
search: {
|
|
54
|
+
conditions: searchConditions
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
: {}),
|
|
58
|
+
sort: { status: 'DESC' }
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
getQueryKey: () => [...queryKey, page],
|
|
62
|
+
queryOptions: {
|
|
63
|
+
suspense: false
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const pagesCount = Math.ceil(data?.meta.total / limit);
|
|
68
|
+
const arePaginationComponentsDisplayed = !equals(pagesCount, 1);
|
|
69
|
+
|
|
70
|
+
const hasNoElements = useMemo(
|
|
71
|
+
() => isEmpty(data?.result) || isNil(data?.result),
|
|
72
|
+
[data]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (hasNoElements) {
|
|
76
|
+
return (
|
|
77
|
+
<div className={classes.notFound}>
|
|
78
|
+
<Typography color="disabled">{t(labelHasNoElements)}</Typography>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={classes.container}>
|
|
85
|
+
<div className={classes.body}>
|
|
86
|
+
{arePaginationComponentsDisplayed && (
|
|
87
|
+
<div className={classes.arrowContainer}>
|
|
88
|
+
<IconButton
|
|
89
|
+
onClick={() => setPage(page - 1)}
|
|
90
|
+
disabled={equals(page, 1)}
|
|
91
|
+
dataTestid={labelPreviousPage}
|
|
92
|
+
className={classes.icon}
|
|
93
|
+
>
|
|
94
|
+
<ArrowBackIcon className={classes.arrow} />
|
|
95
|
+
</IconButton>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<div className={classes.content}>
|
|
100
|
+
{isLoading ? (
|
|
101
|
+
<CircularProgress color="inherit" size={25} />
|
|
102
|
+
) : (
|
|
103
|
+
data?.result.map(({ id, name }) => (
|
|
104
|
+
<Link
|
|
105
|
+
key={id}
|
|
106
|
+
variant="body2"
|
|
107
|
+
className={cx({
|
|
108
|
+
[classes.item]: true,
|
|
109
|
+
[classes.link]: !!onItemClick
|
|
110
|
+
})}
|
|
111
|
+
onClick={() => onItemClick?.({ id })}
|
|
112
|
+
>
|
|
113
|
+
{truncate({
|
|
114
|
+
content: name,
|
|
115
|
+
maxLength: 25
|
|
116
|
+
})}
|
|
117
|
+
</Link>
|
|
118
|
+
))
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{arePaginationComponentsDisplayed && (
|
|
123
|
+
<div className={classes.arrowContainer}>
|
|
124
|
+
<IconButton
|
|
125
|
+
onClick={() => setPage(page + 1)}
|
|
126
|
+
disabled={equals(pagesCount, page)}
|
|
127
|
+
className={classes.icon}
|
|
128
|
+
dataTestid={labelNextPage}
|
|
129
|
+
>
|
|
130
|
+
<ArrowForwardIcon className={classes.arrow} />
|
|
131
|
+
</IconButton>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{arePaginationComponentsDisplayed && (
|
|
137
|
+
<Typography
|
|
138
|
+
className={classes.page}
|
|
139
|
+
variant="body2"
|
|
140
|
+
>{`Page ${page}/${pagesCount}`}</Typography>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default Pagination;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Props as SingleAutocompleteFieldProps } from './InputField/Select/Autocomplete';
|
|
2
2
|
|
|
3
3
|
export { default as IconButton } from './Button/Icon';
|
|
4
|
+
export { default as Pagination } from './Pagination';
|
|
4
5
|
|
|
5
6
|
export { Checkbox, CheckboxGroup } from './Checkbox';
|
|
6
7
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromPairs, startsWith } from 'ramda';
|
|
1
|
+
import { equals, fromPairs, startsWith } from 'ramda';
|
|
2
2
|
|
|
3
3
|
import { QueryParameter } from '../models';
|
|
4
4
|
|
|
@@ -25,7 +25,12 @@ const getUrlQueryParameters = <
|
|
|
25
25
|
|
|
26
26
|
const entries = [...urlParams.entries()].map<[string, string]>(
|
|
27
27
|
([key, value]) => {
|
|
28
|
-
if (
|
|
28
|
+
if (
|
|
29
|
+
startsWith('/', value) ||
|
|
30
|
+
(!equals('false', value) &&
|
|
31
|
+
!equals('true', value) &&
|
|
32
|
+
!equals(value.match(/^[a-zA-Z]/), null))
|
|
33
|
+
) {
|
|
29
34
|
return [key, value];
|
|
30
35
|
}
|
|
31
36
|
|