@centreon/ui 24.4.9 → 24.4.11
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/Button/Icon/index.tsx +1 -1
- package/src/Form/Form.cypress.spec.tsx +133 -0
- package/src/Form/Inputs/List/Content.tsx +62 -0
- package/src/Form/Inputs/List/List.styles.ts +29 -0
- package/src/Form/Inputs/List/List.tsx +58 -0
- package/src/Form/Inputs/List/useList.ts +81 -0
- package/src/Form/Inputs/index.tsx +2 -0
- package/src/Form/Inputs/models.ts +9 -1
- package/src/Listing/Listing.cypress.spec.tsx +2 -2
- package/src/Listing/index.stories.tsx +2 -2
- package/src/Listing/index.tsx +7 -7
- package/src/Typography/Subtitle.tsx +55 -0
- package/src/api/useMutationQuery/index.ts +2 -0
- package/src/components/Modal/Modal.styles.ts +4 -3
- package/src/components/Modal/ModalActions.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/usePluralizedTranslation.ts +21 -0
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { object } from 'yup';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import { useFormikContext } from 'formik';
|
|
4
|
+
|
|
5
|
+
import { Typography } from '@mui/material';
|
|
6
|
+
|
|
7
|
+
import { Button } from '../components';
|
|
8
|
+
|
|
9
|
+
import { Form } from './Form';
|
|
10
|
+
import { InputType } from './Inputs/models';
|
|
11
|
+
|
|
12
|
+
faker.seed(42);
|
|
13
|
+
|
|
14
|
+
const AddItem = ({ addItem }: { addItem: (item) => void }): JSX.Element => {
|
|
15
|
+
const { values } = useFormikContext();
|
|
16
|
+
const add = (): void => {
|
|
17
|
+
addItem({
|
|
18
|
+
alias: faker.company.name(),
|
|
19
|
+
id: values.list.length,
|
|
20
|
+
name: faker.person.firstName()
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Button variant="ghost" onClick={add}>
|
|
26
|
+
Add item
|
|
27
|
+
</Button>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SortContent = ({
|
|
32
|
+
name,
|
|
33
|
+
alias
|
|
34
|
+
}: {
|
|
35
|
+
alias: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}): JSX.Element => (
|
|
38
|
+
<Typography>
|
|
39
|
+
{name} ({alias})
|
|
40
|
+
</Typography>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const initializeFormList = (): void => {
|
|
44
|
+
cy.mount({
|
|
45
|
+
Component: (
|
|
46
|
+
<Form
|
|
47
|
+
initialValues={{
|
|
48
|
+
list: []
|
|
49
|
+
}}
|
|
50
|
+
inputs={[
|
|
51
|
+
{
|
|
52
|
+
fieldName: 'list',
|
|
53
|
+
group: '',
|
|
54
|
+
label: '',
|
|
55
|
+
list: {
|
|
56
|
+
AddItem,
|
|
57
|
+
SortContent,
|
|
58
|
+
addItemLabel: 'Add an item to the list',
|
|
59
|
+
itemProps: ['id', 'name', 'alias'],
|
|
60
|
+
sortLabel: 'Sort items'
|
|
61
|
+
},
|
|
62
|
+
type: InputType.List
|
|
63
|
+
}
|
|
64
|
+
]}
|
|
65
|
+
submit={cy.stub()}
|
|
66
|
+
validationSchema={object()}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
describe('Form list', () => {
|
|
73
|
+
beforeEach(initializeFormList);
|
|
74
|
+
|
|
75
|
+
it('adds an element to the list', () => {
|
|
76
|
+
cy.contains('Add an item to the list').should('be.visible');
|
|
77
|
+
cy.contains('Sort items').should('be.visible');
|
|
78
|
+
|
|
79
|
+
cy.contains('Add item').click();
|
|
80
|
+
|
|
81
|
+
cy.findByLabelText('sort-0').should('be.visible');
|
|
82
|
+
cy.findByLabelText('delete-0').should('be.visible');
|
|
83
|
+
cy.contains('Christelle (Schinner - Wiegand)').should('be.visible');
|
|
84
|
+
|
|
85
|
+
cy.makeSnapshot();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('sorts elements in the list', () => {
|
|
89
|
+
cy.contains('Add an item to the list').should('be.visible');
|
|
90
|
+
cy.contains('Sort items').should('be.visible');
|
|
91
|
+
|
|
92
|
+
cy.contains('Add item').click();
|
|
93
|
+
cy.contains('Add item').click();
|
|
94
|
+
|
|
95
|
+
cy.findByLabelText('sort-0').should('be.visible');
|
|
96
|
+
cy.findByLabelText('delete-0').should('be.visible');
|
|
97
|
+
cy.contains('Carley (Satterfield, Miller and Metz)').should('be.visible');
|
|
98
|
+
cy.findByLabelText('sort-1').should('be.visible');
|
|
99
|
+
cy.findByLabelText('delete-1').should('be.visible');
|
|
100
|
+
cy.contains('Anderson (Crist - Bradtke)').should('be.visible');
|
|
101
|
+
|
|
102
|
+
cy.moveSortableElementUsingAriaLabel({
|
|
103
|
+
ariaLabel: 'sort-0',
|
|
104
|
+
direction: 'down'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
cy.contains('Carley (Satterfield, Miller and Metz)').should('be.visible');
|
|
108
|
+
cy.contains('Anderson (Crist - Bradtke)').should('be.visible');
|
|
109
|
+
|
|
110
|
+
cy.makeSnapshot();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('removes an element from the list', () => {
|
|
114
|
+
cy.contains('Add an item to the list').should('be.visible');
|
|
115
|
+
cy.contains('Sort items').should('be.visible');
|
|
116
|
+
|
|
117
|
+
cy.contains('Add item').click();
|
|
118
|
+
cy.contains('Add item').click();
|
|
119
|
+
|
|
120
|
+
cy.findByLabelText('sort-0').should('be.visible');
|
|
121
|
+
cy.findByLabelText('delete-0').should('be.visible');
|
|
122
|
+
cy.contains('Lea (Streich - Hartmann)').should('be.visible');
|
|
123
|
+
cy.findByLabelText('sort-1').should('be.visible');
|
|
124
|
+
cy.findByLabelText('delete-1').should('be.visible');
|
|
125
|
+
cy.contains('Akeem (Quigley LLC)').should('be.visible');
|
|
126
|
+
|
|
127
|
+
cy.findByLabelText('delete-0').click();
|
|
128
|
+
|
|
129
|
+
cy.contains('Lea (Streich - Hartmann)').should('not.exist');
|
|
130
|
+
|
|
131
|
+
cy.makeSnapshot();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { DraggableSyntheticListeners } from '@dnd-kit/core';
|
|
4
|
+
|
|
5
|
+
import KrilinIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
6
|
+
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
|
7
|
+
|
|
8
|
+
import { IconButton } from '../../../components';
|
|
9
|
+
|
|
10
|
+
import { useListStyles } from './List.styles';
|
|
11
|
+
|
|
12
|
+
export interface ContentProps {
|
|
13
|
+
attributes;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
deleteItem: (id: string) => () => void;
|
|
16
|
+
id: string;
|
|
17
|
+
isDragging: boolean;
|
|
18
|
+
isInDragOverlay?: boolean;
|
|
19
|
+
itemRef: React.RefObject<HTMLDivElement>;
|
|
20
|
+
listeners: DraggableSyntheticListeners;
|
|
21
|
+
name: string;
|
|
22
|
+
style;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Content = ({
|
|
26
|
+
listeners,
|
|
27
|
+
itemRef,
|
|
28
|
+
attributes,
|
|
29
|
+
style,
|
|
30
|
+
isDragging,
|
|
31
|
+
id,
|
|
32
|
+
children,
|
|
33
|
+
deleteItem
|
|
34
|
+
}: ContentProps): JSX.Element => {
|
|
35
|
+
const { classes } = useListStyles();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={classes.content}
|
|
40
|
+
ref={itemRef}
|
|
41
|
+
{...attributes}
|
|
42
|
+
style={style}
|
|
43
|
+
>
|
|
44
|
+
<IconButton
|
|
45
|
+
data-dragging={isDragging}
|
|
46
|
+
size="small"
|
|
47
|
+
{...listeners}
|
|
48
|
+
aria-label={`sort-${id}`}
|
|
49
|
+
icon={<KrilinIndicatorIcon fontSize="small" />}
|
|
50
|
+
/>
|
|
51
|
+
<div className={classes.innerContent}>{children}</div>
|
|
52
|
+
<IconButton
|
|
53
|
+
aria-label={`delete-${id}`}
|
|
54
|
+
icon={<DeleteOutlineIcon color="error" fontSize="small" />}
|
|
55
|
+
size="small"
|
|
56
|
+
onClick={deleteItem(id)}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default Content;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { makeStyles } from 'tss-react/mui';
|
|
2
|
+
|
|
3
|
+
export const useListStyles = makeStyles()((theme) => ({
|
|
4
|
+
content: {
|
|
5
|
+
'& [data-dragging="false"]': {
|
|
6
|
+
cursor: 'grab'
|
|
7
|
+
},
|
|
8
|
+
'& [data-dragging="true"]': {
|
|
9
|
+
cursor: 'grabbing'
|
|
10
|
+
},
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
borderBottom: `1px dashed ${theme.palette.action.disabledBackground}`,
|
|
13
|
+
display: 'flex',
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
padding: theme.spacing(1, 0)
|
|
16
|
+
},
|
|
17
|
+
innerContent: {
|
|
18
|
+
flexGrow: 1
|
|
19
|
+
},
|
|
20
|
+
items: {
|
|
21
|
+
maxHeight: theme.spacing(16),
|
|
22
|
+
overflowY: 'auto'
|
|
23
|
+
},
|
|
24
|
+
list: {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
flexDirection: 'column',
|
|
27
|
+
gap: theme.spacing(1)
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
import { closestCenter } from '@dnd-kit/core';
|
|
4
|
+
import { verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import { InputPropsWithoutGroup } from '../models';
|
|
8
|
+
import { SortableItems, Subtitle } from '../../..';
|
|
9
|
+
|
|
10
|
+
import { useList } from './useList';
|
|
11
|
+
import { useListStyles } from './List.styles';
|
|
12
|
+
import Content, { ContentProps } from './Content';
|
|
13
|
+
|
|
14
|
+
const List = ({
|
|
15
|
+
list,
|
|
16
|
+
fieldName
|
|
17
|
+
}: InputPropsWithoutGroup): JSX.Element | null => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const { classes } = useListStyles();
|
|
20
|
+
|
|
21
|
+
const { addItem, sortList, sortedList, deleteItem } = useList({ fieldName });
|
|
22
|
+
|
|
23
|
+
const { AddItem, addItemLabel, sortLabel, SortContent, itemProps } = list as {
|
|
24
|
+
AddItem: ComponentType<{ addItem }>;
|
|
25
|
+
SortContent: ComponentType;
|
|
26
|
+
addItemLabel?: string;
|
|
27
|
+
itemProps: Array<string>;
|
|
28
|
+
sortLabel?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={classes.list}>
|
|
33
|
+
{addItemLabel && <Subtitle>{t(addItemLabel)}</Subtitle>}
|
|
34
|
+
<AddItem addItem={addItem} />
|
|
35
|
+
{sortLabel && <Subtitle>{t(sortLabel)}</Subtitle>}
|
|
36
|
+
<div className={classes.items}>
|
|
37
|
+
<SortableItems
|
|
38
|
+
updateSortableItemsOnItemsChange
|
|
39
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
40
|
+
Content={(props: Omit<ContentProps, 'children' | 'deleteItem'>) => (
|
|
41
|
+
<Content {...props} deleteItem={deleteItem}>
|
|
42
|
+
<SortContent {...props} />
|
|
43
|
+
</Content>
|
|
44
|
+
)}
|
|
45
|
+
collisionDetection={closestCenter}
|
|
46
|
+
itemProps={itemProps}
|
|
47
|
+
items={sortedList}
|
|
48
|
+
sortingStrategy={verticalListSortingStrategy}
|
|
49
|
+
onDragEnd={({ items }): void => {
|
|
50
|
+
sortList(items);
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default List;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useMemo, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { FormikValues, useFormikContext } from 'formik';
|
|
4
|
+
import {
|
|
5
|
+
append,
|
|
6
|
+
equals,
|
|
7
|
+
inc,
|
|
8
|
+
isEmpty,
|
|
9
|
+
pluck,
|
|
10
|
+
prop,
|
|
11
|
+
reject,
|
|
12
|
+
sortBy
|
|
13
|
+
} from 'ramda';
|
|
14
|
+
|
|
15
|
+
import { SelectEntry } from '../../..';
|
|
16
|
+
|
|
17
|
+
interface UseListState {
|
|
18
|
+
addItem: (newItem: SelectEntry) => void;
|
|
19
|
+
deleteItem: (id: string) => () => void;
|
|
20
|
+
sortList: (items: Array<string>) => void;
|
|
21
|
+
sortedList: Array<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const useList = ({ fieldName }): UseListState => {
|
|
25
|
+
const { values, setFieldValue } = useFormikContext<FormikValues>();
|
|
26
|
+
const maxOrder = useRef(0);
|
|
27
|
+
|
|
28
|
+
const list = values[fieldName];
|
|
29
|
+
|
|
30
|
+
const sortedList = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
sortBy(prop('order'), list).map(({ id, ...props }) => ({
|
|
33
|
+
id: `${id}`,
|
|
34
|
+
...props
|
|
35
|
+
})),
|
|
36
|
+
[list]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const addItem = (newItem: SelectEntry): void => {
|
|
40
|
+
setFieldValue(
|
|
41
|
+
fieldName,
|
|
42
|
+
append(
|
|
43
|
+
{
|
|
44
|
+
...newItem,
|
|
45
|
+
id: (newItem as SelectEntry).id as number,
|
|
46
|
+
order: inc(maxOrder.current)
|
|
47
|
+
},
|
|
48
|
+
list
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const deleteItem = (id: string) => (): void => {
|
|
54
|
+
const newItems = reject((item) => equals(Number(id), item.id))(list);
|
|
55
|
+
|
|
56
|
+
setFieldValue(fieldName, newItems);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const sortList = (items: Array<string>): void => {
|
|
60
|
+
const newOrderedList = items.map((itemId, idx) => {
|
|
61
|
+
const item = sortedList.find(({ id }) => equals(id, itemId));
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...item,
|
|
65
|
+
id: Number(item?.id),
|
|
66
|
+
order: inc(idx)
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
setFieldValue(fieldName, newOrderedList);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
maxOrder.current = isEmpty(list) ? 0 : Math.max(...pluck('order', list));
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
addItem,
|
|
77
|
+
deleteItem,
|
|
78
|
+
sortList,
|
|
79
|
+
sortedList
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -21,6 +21,7 @@ import CheckboxGroup from './CheckboxGroup';
|
|
|
21
21
|
import Checkbox from './Checkbox';
|
|
22
22
|
import Custom from './Custom';
|
|
23
23
|
import LoadingSkeleton from './LoadingSkeleton';
|
|
24
|
+
import List from './List/List';
|
|
24
25
|
|
|
25
26
|
export const getInput = R.cond<
|
|
26
27
|
Array<InputType>,
|
|
@@ -66,6 +67,7 @@ export const getInput = R.cond<
|
|
|
66
67
|
R.equals(InputType.CheckboxGroup) as (b: InputType) => boolean,
|
|
67
68
|
R.always(CheckboxGroup)
|
|
68
69
|
],
|
|
70
|
+
[R.equals(InputType.List) as (b: InputType) => boolean, R.always(List)],
|
|
69
71
|
[R.T, R.always(TextInput)]
|
|
70
72
|
]);
|
|
71
73
|
|
|
@@ -18,7 +18,8 @@ export enum InputType {
|
|
|
18
18
|
Grid,
|
|
19
19
|
Custom,
|
|
20
20
|
Checkbox,
|
|
21
|
-
CheckboxGroup
|
|
21
|
+
CheckboxGroup,
|
|
22
|
+
List
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface FieldsTableGetRequiredProps {
|
|
@@ -76,6 +77,13 @@ export interface InputProps {
|
|
|
76
77
|
hideInput?: (values: FormikValues) => boolean;
|
|
77
78
|
inputClassName?: string;
|
|
78
79
|
label: string;
|
|
80
|
+
list?: {
|
|
81
|
+
AddItem: React.ComponentType<{ addItem }>;
|
|
82
|
+
SortContent: React.ComponentType<object>;
|
|
83
|
+
addItemLabel?: string;
|
|
84
|
+
itemProps: Array<string>;
|
|
85
|
+
sortLabel?: string;
|
|
86
|
+
};
|
|
79
87
|
radio?: {
|
|
80
88
|
options?: Array<{
|
|
81
89
|
label: string | JSX.Element;
|
|
@@ -77,9 +77,9 @@ const mountListing = (): void => {
|
|
|
77
77
|
subItems={{
|
|
78
78
|
canCheckSubItems: false,
|
|
79
79
|
enable: true,
|
|
80
|
+
getRowProperty: () => 'subItems',
|
|
80
81
|
labelCollapse: 'Collapse',
|
|
81
|
-
labelExpand: 'Expand'
|
|
82
|
-
rowProperty: 'subItems'
|
|
82
|
+
labelExpand: 'Expand'
|
|
83
83
|
}}
|
|
84
84
|
totalRows={10}
|
|
85
85
|
/>
|
|
@@ -387,9 +387,9 @@ export const ListingWithSubItems = {
|
|
|
387
387
|
subItems: {
|
|
388
388
|
canCheckSubItems: false,
|
|
389
389
|
enable: true,
|
|
390
|
+
getRowProperty: () => 'subItems',
|
|
390
391
|
labelCollapse: 'Collapse',
|
|
391
|
-
labelExpand: 'Expand'
|
|
392
|
-
rowProperty: 'subItems'
|
|
392
|
+
labelExpand: 'Expand'
|
|
393
393
|
},
|
|
394
394
|
totalRows: 10
|
|
395
395
|
},
|
package/src/Listing/index.tsx
CHANGED
|
@@ -121,9 +121,9 @@ export interface Props<TRow> {
|
|
|
121
121
|
subItems?: {
|
|
122
122
|
canCheckSubItems: boolean;
|
|
123
123
|
enable: boolean;
|
|
124
|
+
getRowProperty: (row?) => string;
|
|
124
125
|
labelCollapse: string;
|
|
125
126
|
labelExpand: string;
|
|
126
|
-
rowProperty: string;
|
|
127
127
|
};
|
|
128
128
|
totalRows?: number;
|
|
129
129
|
viewerModeConfiguration?: ViewerModeConfiguration;
|
|
@@ -176,9 +176,9 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
176
176
|
subItems = {
|
|
177
177
|
canCheckSubItems: false,
|
|
178
178
|
enable: false,
|
|
179
|
+
getRowProperty: () => '',
|
|
179
180
|
labelCollapse: 'Collapse',
|
|
180
|
-
labelExpand: 'Expand'
|
|
181
|
-
rowProperty: ''
|
|
181
|
+
labelExpand: 'Expand'
|
|
182
182
|
}
|
|
183
183
|
}: Props<TRow>): JSX.Element => {
|
|
184
184
|
const currentVisibleColumns = getVisibleColumns({
|
|
@@ -211,10 +211,10 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
211
211
|
? reduce<TRow, Array<TRow>>(
|
|
212
212
|
(acc, row): Array<TRow> => {
|
|
213
213
|
if (
|
|
214
|
-
row[subItems.
|
|
214
|
+
row[subItems.getRowProperty()] &&
|
|
215
215
|
subItemsPivots.includes(row.id)
|
|
216
216
|
) {
|
|
217
|
-
return [...acc, row, ...row[subItems.
|
|
217
|
+
return [...acc, row, ...row[subItems.getRowProperty()]];
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
return [...acc, row];
|
|
@@ -449,7 +449,7 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
449
449
|
reduce<TRow | number, Array<string | number>>(
|
|
450
450
|
(acc, row) => [
|
|
451
451
|
...acc,
|
|
452
|
-
...pluck('id', row[subItems?.
|
|
452
|
+
...pluck('id', row[subItems?.getRowProperty() || ''] || [])
|
|
453
453
|
],
|
|
454
454
|
[],
|
|
455
455
|
rows
|
|
@@ -620,7 +620,7 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
620
620
|
listingVariant={listingVariant}
|
|
621
621
|
row={row}
|
|
622
622
|
rowColorConditions={rowColorConditions}
|
|
623
|
-
subItemsRowProperty={subItems?.
|
|
623
|
+
subItemsRowProperty={subItems?.getRowProperty(row)}
|
|
624
624
|
/>
|
|
625
625
|
))}
|
|
626
626
|
</ListingRow>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type, equals } from 'ramda';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
import { Typography } from '@mui/material';
|
|
7
|
+
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
|
8
|
+
|
|
9
|
+
import { Tooltip } from '../components';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
secondaryLabel?: string | Array<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Subtitle = ({ children, secondaryLabel }: Props): JSX.Element => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
|
|
19
|
+
const containsSeveralSecondaryLabels = equals(type(secondaryLabel), 'Array');
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Typography variant="subtitle1">
|
|
23
|
+
<strong>{children}</strong>
|
|
24
|
+
{secondaryLabel && (
|
|
25
|
+
<Tooltip
|
|
26
|
+
aria-label={
|
|
27
|
+
containsSeveralSecondaryLabels
|
|
28
|
+
? secondaryLabel[0]
|
|
29
|
+
: (secondaryLabel as string)
|
|
30
|
+
}
|
|
31
|
+
followCursor={false}
|
|
32
|
+
label={
|
|
33
|
+
containsSeveralSecondaryLabels ? (
|
|
34
|
+
<>
|
|
35
|
+
{secondaryLabel.map((label) => (
|
|
36
|
+
<p key={label}>{t(label)}</p>
|
|
37
|
+
))}
|
|
38
|
+
</>
|
|
39
|
+
) : (
|
|
40
|
+
t(secondaryLabel)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
placement="right"
|
|
44
|
+
>
|
|
45
|
+
<HelpOutlineIcon
|
|
46
|
+
color="primary"
|
|
47
|
+
sx={{ ml: 1, verticalAlign: 'middle' }}
|
|
48
|
+
/>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
)}
|
|
51
|
+
</Typography>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default Subtitle;
|
|
@@ -70,6 +70,7 @@ const useMutationQuery = <T extends object, TMeta>({
|
|
|
70
70
|
onMutate,
|
|
71
71
|
onError,
|
|
72
72
|
onSuccess,
|
|
73
|
+
onSettled,
|
|
73
74
|
baseEndpoint
|
|
74
75
|
}: UseMutationQueryProps<T, TMeta>): UseMutationQueryState<T> => {
|
|
75
76
|
const { showErrorMessage } = useSnackbar();
|
|
@@ -101,6 +102,7 @@ const useMutationQuery = <T extends object, TMeta>({
|
|
|
101
102
|
},
|
|
102
103
|
onError,
|
|
103
104
|
onMutate,
|
|
105
|
+
onSettled,
|
|
104
106
|
onSuccess
|
|
105
107
|
});
|
|
106
108
|
|
|
@@ -49,14 +49,15 @@ const useStyles = makeStyles<{
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
modalActions: {
|
|
52
|
-
|
|
52
|
+
'&[data-fixed="true"]': {
|
|
53
|
+
position: 'fixed'
|
|
54
|
+
},
|
|
53
55
|
bottom: theme.spacing(2),
|
|
54
56
|
display: 'flex',
|
|
55
57
|
flexDirection: 'row',
|
|
56
58
|
gap: theme.spacing(2),
|
|
57
59
|
justifyContent: 'flex-end',
|
|
58
|
-
|
|
59
|
-
right: theme.spacing(2)
|
|
60
|
+
right: theme.spacing(2.5)
|
|
60
61
|
},
|
|
61
62
|
modalBody: {
|
|
62
63
|
'& > p': {
|
|
@@ -8,6 +8,7 @@ export type ModalActionsProps = {
|
|
|
8
8
|
children?: React.ReactNode;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
isDanger?: boolean;
|
|
11
|
+
isFixed?: boolean;
|
|
11
12
|
labels?: ModalActionsLabels;
|
|
12
13
|
onCancel?: () => void;
|
|
13
14
|
onConfirm?: () => void;
|
|
@@ -24,12 +25,13 @@ const ModalActions = ({
|
|
|
24
25
|
onCancel,
|
|
25
26
|
onConfirm,
|
|
26
27
|
isDanger = false,
|
|
27
|
-
disabled
|
|
28
|
+
disabled,
|
|
29
|
+
isFixed
|
|
28
30
|
}: ModalActionsProps): ReactElement => {
|
|
29
31
|
const { classes } = useStyles();
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
|
-
<div className={classes.modalActions}>
|
|
34
|
+
<div className={classes.modalActions} data-fixed={isFixed}>
|
|
33
35
|
{children || (
|
|
34
36
|
<>
|
|
35
37
|
<Button
|
package/src/index.ts
CHANGED
|
@@ -164,3 +164,4 @@ export { default as TimePeriods } from './TimePeriods';
|
|
|
164
164
|
export { default as SimpleCustomTimePeriod } from './TimePeriods/CustomTimePeriod/SimpleCustomTimePeriod';
|
|
165
165
|
export { default as DateTimePickerInput } from './TimePeriods/DateTimePickerInput';
|
|
166
166
|
export * from './ParentSize';
|
|
167
|
+
export { default as Subtitle } from './Typography/Subtitle';
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import pluralize from 'pluralize';
|
|
3
|
+
|
|
4
|
+
interface TProps {
|
|
5
|
+
count: number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const usePluralizedTranslation = (): {
|
|
10
|
+
pluralizedT: (props: TProps) => string;
|
|
11
|
+
} => {
|
|
12
|
+
const translation = useTranslation();
|
|
13
|
+
|
|
14
|
+
const pluralizedT = ({ label, count }: TProps): string => {
|
|
15
|
+
return pluralize(translation.t(label), count);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
pluralizedT
|
|
20
|
+
};
|
|
21
|
+
};
|