@centreon/ui 23.10.40 → 23.10.42
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 +2 -1
- package/src/Listing/Cell/DataCell.tsx +16 -4
- package/src/Listing/Cell/index.tsx +82 -2
- package/src/Listing/Listing.cypress.spec.tsx +120 -0
- package/src/Listing/Listing.styles.ts +2 -2
- package/src/Listing/ListingDocs.mdx +61 -0
- package/src/Listing/Row/Row.tsx +5 -2
- package/src/Listing/index.stories.tsx +104 -31
- package/src/Listing/index.tsx +103 -33
- package/src/Listing/models.ts +1 -0
- package/src/Listing/tableAtoms.ts +2 -0
- package/src/Panel/index.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@centreon/ui",
|
|
3
|
-
"version": "23.10.
|
|
3
|
+
"version": "23.10.42",
|
|
4
4
|
"description": "Centreon UI Components",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@storybook/addon-essentials": "^7.0.9",
|
|
57
57
|
"@storybook/addon-interactions": "^7.0.9",
|
|
58
58
|
"@storybook/addon-styling": "^1.0.6",
|
|
59
|
+
"@storybook/blocks": "^7.0.26",
|
|
59
60
|
"@storybook/builder-vite": "^7.0.9",
|
|
60
61
|
"@storybook/jest": "^0.1.0",
|
|
61
62
|
"@storybook/manager-api": "^7.0.9",
|
|
@@ -25,8 +25,11 @@ interface Props {
|
|
|
25
25
|
getHighlightRowCondition?: (row) => boolean;
|
|
26
26
|
isRowHovered: boolean;
|
|
27
27
|
isRowSelected: boolean;
|
|
28
|
+
labelCollapse?: string;
|
|
29
|
+
labelExpand?: string;
|
|
28
30
|
row?;
|
|
29
31
|
rowColorConditions?: Array<RowColorCondition>;
|
|
32
|
+
subItemsRowProperty?: string;
|
|
30
33
|
viewMode?: ListingVariant;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -38,16 +41,23 @@ const DataCell = ({
|
|
|
38
41
|
rowColorConditions,
|
|
39
42
|
disableRowCondition,
|
|
40
43
|
viewMode,
|
|
41
|
-
getHighlightRowCondition
|
|
44
|
+
getHighlightRowCondition,
|
|
45
|
+
subItemsRowProperty,
|
|
46
|
+
labelCollapse,
|
|
47
|
+
labelExpand
|
|
42
48
|
}: Props): JSX.Element | null => {
|
|
43
49
|
const { classes } = useStyles();
|
|
44
50
|
const { dataStyle } = useStyleTable({ viewMode });
|
|
45
51
|
|
|
46
52
|
const commonCellProps = {
|
|
47
53
|
disableRowCondition,
|
|
54
|
+
displaySubItemsCaret: column.displaySubItemsCaret,
|
|
48
55
|
isRowHovered,
|
|
56
|
+
labelCollapse,
|
|
57
|
+
labelExpand,
|
|
49
58
|
row,
|
|
50
|
-
rowColorConditions
|
|
59
|
+
rowColorConditions,
|
|
60
|
+
subItemsRowProperty
|
|
51
61
|
};
|
|
52
62
|
|
|
53
63
|
const isRowHighlighted = getHighlightRowCondition?.(row);
|
|
@@ -106,7 +116,7 @@ const DataCell = ({
|
|
|
106
116
|
isRowHighlighted={isRowHighlighted}
|
|
107
117
|
viewMode={viewMode}
|
|
108
118
|
onClick={(e): void => {
|
|
109
|
-
if (!clickable) {
|
|
119
|
+
if (!clickable || !column.displaySubItemsCaret) {
|
|
110
120
|
return;
|
|
111
121
|
}
|
|
112
122
|
e.preventDefault();
|
|
@@ -223,6 +233,7 @@ const MemoizedDataCell = memo<Props>(
|
|
|
223
233
|
: nextProps.row;
|
|
224
234
|
|
|
225
235
|
return (
|
|
236
|
+
equals(prevProps.row, nextProps.row) &&
|
|
226
237
|
equals(previousIsComponentHovered, nextIsComponentHovered) &&
|
|
227
238
|
equals(prevProps.isRowHovered, nextProps.isRowHovered) &&
|
|
228
239
|
equals(previousFormattedString, nextFormattedString) &&
|
|
@@ -244,7 +255,8 @@ const MemoizedDataCell = memo<Props>(
|
|
|
244
255
|
nextProps.disableRowCondition(nextProps.row)
|
|
245
256
|
) &&
|
|
246
257
|
equals(previousIsRowHighlighted, nextIsRowHighlighted) &&
|
|
247
|
-
equals(prevProps.viewMode, nextProps.viewMode)
|
|
258
|
+
equals(prevProps.viewMode, nextProps.viewMode) &&
|
|
259
|
+
equals(prevProps.subItemsRowProperty, nextProps.subItemsRowProperty)
|
|
248
260
|
);
|
|
249
261
|
}
|
|
250
262
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import { isNil, omit } from 'ramda';
|
|
3
|
+
import { append, equals, includes, isNil, omit, reject } from 'ramda';
|
|
4
4
|
import { makeStyles } from 'tss-react/mui';
|
|
5
5
|
import { CSSObject } from 'tss-react';
|
|
6
|
+
import { useAtom } from 'jotai';
|
|
6
7
|
|
|
8
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
7
9
|
import {
|
|
8
10
|
alpha,
|
|
9
11
|
TableCell,
|
|
@@ -14,7 +16,9 @@ import {
|
|
|
14
16
|
|
|
15
17
|
import { ListingVariant } from '@centreon/ui-context';
|
|
16
18
|
|
|
19
|
+
import { IconButton } from '../..';
|
|
17
20
|
import { getTextStyleByViewMode } from '../useStyleTable';
|
|
21
|
+
import { subItemsPivotsAtom } from '../tableAtoms';
|
|
18
22
|
|
|
19
23
|
import { Props as DataCellProps } from './DataCell';
|
|
20
24
|
|
|
@@ -80,6 +84,20 @@ const useStyles = makeStyles<StylesProps>()(
|
|
|
80
84
|
viewMode
|
|
81
85
|
}
|
|
82
86
|
) => ({
|
|
87
|
+
caret: {
|
|
88
|
+
transition: theme.transitions.create('transform', {
|
|
89
|
+
duration: theme.transitions.duration.short
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
caretLess: {
|
|
93
|
+
transform: 'rotate3d(0,0,1,180deg)'
|
|
94
|
+
},
|
|
95
|
+
caretMore: {
|
|
96
|
+
transform: 'rotate3d(0,0,1,0deg)'
|
|
97
|
+
},
|
|
98
|
+
fakeCaret: {
|
|
99
|
+
marginLeft: theme.spacing(3)
|
|
100
|
+
},
|
|
83
101
|
root: {
|
|
84
102
|
alignItems: 'center',
|
|
85
103
|
backgroundColor: getBackgroundColor({
|
|
@@ -111,15 +129,57 @@ interface Props
|
|
|
111
129
|
'isRowHovered' | 'row' | 'rowColorConditions' | 'disableRowCondition'
|
|
112
130
|
>,
|
|
113
131
|
TableCellProps {
|
|
132
|
+
displaySubItemsCaret?: boolean;
|
|
114
133
|
isRowHighlighted?: boolean;
|
|
134
|
+
labelCollapse?: string;
|
|
135
|
+
labelExpand?: string;
|
|
136
|
+
subItemsRowProperty?: string;
|
|
115
137
|
viewMode?: ListingVariant;
|
|
116
138
|
}
|
|
117
139
|
|
|
118
|
-
const
|
|
140
|
+
const isPivotExistInTheList = (
|
|
141
|
+
id
|
|
142
|
+
): ((list: Array<number | string>) => boolean) => includes(id);
|
|
143
|
+
|
|
144
|
+
const handleSubItems = ({
|
|
145
|
+
currentSubItemsPivots,
|
|
146
|
+
id
|
|
147
|
+
}): Array<number | string> => {
|
|
148
|
+
if (isPivotExistInTheList(id)(currentSubItemsPivots)) {
|
|
149
|
+
return reject(equals(id), currentSubItemsPivots);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return append(id, currentSubItemsPivots);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const Cell = ({
|
|
156
|
+
displaySubItemsCaret,
|
|
157
|
+
subItemsRowProperty,
|
|
158
|
+
labelCollapse,
|
|
159
|
+
labelExpand,
|
|
160
|
+
...props
|
|
161
|
+
}: Props): JSX.Element => {
|
|
119
162
|
const { classes, cx } = useStyles(props);
|
|
120
163
|
|
|
164
|
+
const [subItemsPivots, setSubItemsPivots] = useAtom(subItemsPivotsAtom);
|
|
165
|
+
|
|
121
166
|
const { children } = props;
|
|
122
167
|
|
|
168
|
+
const rowId = props.row?.id;
|
|
169
|
+
|
|
170
|
+
const click = (e): void => {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
|
|
174
|
+
setSubItemsPivots((currentSubItemsPivots) =>
|
|
175
|
+
handleSubItems({ currentSubItemsPivots, id: rowId })
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isSubItemsExpanded = isPivotExistInTheList(rowId)(subItemsPivots);
|
|
180
|
+
|
|
181
|
+
const hasSubItems = subItemsRowProperty && props.row[subItemsRowProperty];
|
|
182
|
+
|
|
123
183
|
return (
|
|
124
184
|
<TableCell
|
|
125
185
|
classes={{
|
|
@@ -138,6 +198,26 @@ const Cell = (props: Props): JSX.Element => {
|
|
|
138
198
|
props
|
|
139
199
|
)}
|
|
140
200
|
>
|
|
201
|
+
{displaySubItemsCaret &&
|
|
202
|
+
(hasSubItems ? (
|
|
203
|
+
<IconButton
|
|
204
|
+
ariaLabel={`${isSubItemsExpanded ? labelCollapse : labelExpand} ${
|
|
205
|
+
props.row.id
|
|
206
|
+
}`}
|
|
207
|
+
size="small"
|
|
208
|
+
onClick={click}
|
|
209
|
+
>
|
|
210
|
+
<ExpandMoreIcon
|
|
211
|
+
className={cx(
|
|
212
|
+
classes.caret,
|
|
213
|
+
isSubItemsExpanded ? classes.caretMore : classes.caretLess
|
|
214
|
+
)}
|
|
215
|
+
fontSize="small"
|
|
216
|
+
/>
|
|
217
|
+
</IconButton>
|
|
218
|
+
) : (
|
|
219
|
+
<div className={classes.fakeCaret} />
|
|
220
|
+
))}
|
|
141
221
|
{children}
|
|
142
222
|
</TableCell>
|
|
143
223
|
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Button } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { ColumnType } from './models';
|
|
4
|
+
|
|
5
|
+
import Listing from '.';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
row: { name: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ButtonColumn = ({ row }: Props): JSX.Element => (
|
|
12
|
+
<Button size="small">Click to reveal details about {row.name}</Button>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const generateSubItems = (parentIndex: number): Array<unknown> => {
|
|
16
|
+
return tenElements.map((__, subIndex) => ({
|
|
17
|
+
active: false,
|
|
18
|
+
description: `Sub item ${subIndex + (parentIndex + 10) * 10} description`,
|
|
19
|
+
disableCheckbox: false,
|
|
20
|
+
disableRow: false,
|
|
21
|
+
id: subIndex + (parentIndex + 10) * 10,
|
|
22
|
+
name: `Sub Item ${subIndex + (parentIndex + 10) * 10}`,
|
|
23
|
+
selected: false
|
|
24
|
+
}));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const tenElements = new Array(10).fill(0);
|
|
28
|
+
|
|
29
|
+
const listingWithSubItems = tenElements.map((_, index) => ({
|
|
30
|
+
active: false,
|
|
31
|
+
description: `Entity ${index}`,
|
|
32
|
+
disableCheckbox: false,
|
|
33
|
+
disableRow: false,
|
|
34
|
+
id: index,
|
|
35
|
+
name: `E${index}`,
|
|
36
|
+
selected: false,
|
|
37
|
+
subItems: index % 2 === 0 ? generateSubItems(index) : undefined
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const columnsWithSubItems = [
|
|
41
|
+
{
|
|
42
|
+
getFormattedString: ({ name }): string => name,
|
|
43
|
+
id: 'name',
|
|
44
|
+
label: 'Name',
|
|
45
|
+
type: ColumnType.string
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
getFormattedString: ({ description }): string => description,
|
|
49
|
+
id: 'description',
|
|
50
|
+
label: 'Description',
|
|
51
|
+
type: ColumnType.string
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
Component: ButtonColumn,
|
|
55
|
+
displaySubItemsCaret: true,
|
|
56
|
+
id: '#',
|
|
57
|
+
label: 'Custom',
|
|
58
|
+
type: ColumnType.component,
|
|
59
|
+
width: '350px'
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const expandedItems = [0, 8];
|
|
64
|
+
|
|
65
|
+
const mountListing = (): void => {
|
|
66
|
+
cy.viewport('macbook-13');
|
|
67
|
+
|
|
68
|
+
cy.mount({
|
|
69
|
+
Component: (
|
|
70
|
+
<Listing
|
|
71
|
+
checkable
|
|
72
|
+
columns={columnsWithSubItems}
|
|
73
|
+
currentPage={1}
|
|
74
|
+
limit={10}
|
|
75
|
+
rows={listingWithSubItems}
|
|
76
|
+
subItems={{
|
|
77
|
+
canCheckSubItems: false,
|
|
78
|
+
enable: true,
|
|
79
|
+
labelCollapse: 'Collapse',
|
|
80
|
+
labelExpand: 'Expand',
|
|
81
|
+
rowProperty: 'subItems'
|
|
82
|
+
}}
|
|
83
|
+
totalRows={10}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe('Listing', () => {
|
|
90
|
+
beforeEach(mountListing);
|
|
91
|
+
|
|
92
|
+
it('expands the row when the corresponding icon si clicked', () => {
|
|
93
|
+
cy.contains('E0').should('be.visible');
|
|
94
|
+
|
|
95
|
+
expandedItems.forEach((index) => {
|
|
96
|
+
const subItems = generateSubItems(index);
|
|
97
|
+
|
|
98
|
+
cy.findByLabelText(`Expand ${index}`).click();
|
|
99
|
+
|
|
100
|
+
cy.findByLabelText(`Collapse ${index}`).should('exist');
|
|
101
|
+
|
|
102
|
+
subItems.forEach(({ name, description }) => {
|
|
103
|
+
cy.contains(name).should('exist');
|
|
104
|
+
cy.contains(description).should('exist');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
cy.matchImageSnapshot();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('collapses the row when the corresponding icon si clicked', () => {
|
|
112
|
+
cy.contains('Sub item 100').should('be.visible');
|
|
113
|
+
|
|
114
|
+
cy.findByLabelText('Collapse 0').click();
|
|
115
|
+
|
|
116
|
+
cy.contains('Sub item 100').should('not.exist');
|
|
117
|
+
|
|
118
|
+
cy.matchImageSnapshot();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -14,7 +14,7 @@ interface StylesProps {
|
|
|
14
14
|
viewMode: ListingVariant;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const useListingStyles = makeStyles<StylesProps>()(
|
|
18
18
|
(theme, { dataStyle, getGridTemplateColumn, rows, limit }) => ({
|
|
19
19
|
actionBar: {
|
|
20
20
|
alignItems: 'center',
|
|
@@ -73,4 +73,4 @@ const useStyles = makeStyles<StylesProps>()(
|
|
|
73
73
|
})
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
export {
|
|
76
|
+
export { useListingStyles, loadingIndicatorHeight };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{/* ListingDocs.mdx */}
|
|
2
|
+
|
|
3
|
+
import { Meta, Story } from '@storybook/blocks';
|
|
4
|
+
import * as ListingStories from './index.stories';
|
|
5
|
+
|
|
6
|
+
<Meta of={ListingStories} />
|
|
7
|
+
|
|
8
|
+
# Listing
|
|
9
|
+
|
|
10
|
+
## Expand/collapse rows
|
|
11
|
+
|
|
12
|
+
The listing supports rows expanding and collapsing.
|
|
13
|
+
|
|
14
|
+
In order to use this feature, please follow the instructions:
|
|
15
|
+
- (pre-requisite): Your data should have a child property that contains sub-items.
|
|
16
|
+
```javascript
|
|
17
|
+
[
|
|
18
|
+
{
|
|
19
|
+
"id": 1,
|
|
20
|
+
"festival": "Test",
|
|
21
|
+
"name": "Tom",
|
|
22
|
+
"counter": 3,
|
|
23
|
+
"description": "My description of Tom"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": 2,
|
|
27
|
+
"name": "Jane's group",
|
|
28
|
+
"festival": "fest",
|
|
29
|
+
"counter": 5,
|
|
30
|
+
"description": "My description of Jane's group",
|
|
31
|
+
"subItems": [
|
|
32
|
+
{
|
|
33
|
+
"id": 3,
|
|
34
|
+
"name": "Jane",
|
|
35
|
+
"counter": 0,
|
|
36
|
+
"description": "My description of Jane"
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
- Use the `subItems` property to configure how the component will handle your sub-items
|
|
43
|
+
```javascript
|
|
44
|
+
subItems={{
|
|
45
|
+
canCheckSubItems: false, // Display checkbox on sub items. Default: false
|
|
46
|
+
enable: true, // It enables or disable the row expand/collapse feature. Default: false
|
|
47
|
+
rowProperty: 'subItems' // Which property should be used to detect sub-items. Default: ''
|
|
48
|
+
labelExpand: 'Expand',
|
|
49
|
+
labelCollapse: 'Collapse'
|
|
50
|
+
}}
|
|
51
|
+
```
|
|
52
|
+
- Finally, update the column configuration in order to display the expand/collapse caret
|
|
53
|
+
```javascript
|
|
54
|
+
{
|
|
55
|
+
Component: ButtonColumn,
|
|
56
|
+
displaySubItemsCaret: true, // Add this property
|
|
57
|
+
id: '#',
|
|
58
|
+
label: 'Custom',
|
|
59
|
+
type: ColumnType.component
|
|
60
|
+
}
|
|
61
|
+
```
|
package/src/Listing/Row/Row.tsx
CHANGED
|
@@ -49,6 +49,7 @@ type Props = {
|
|
|
49
49
|
row;
|
|
50
50
|
rowColorConditions: Array<RowColorCondition>;
|
|
51
51
|
shiftKeyDownRowPivot: number | null;
|
|
52
|
+
subItemsPivots: Array<number | string>;
|
|
52
53
|
viewMode?: ListingVariant;
|
|
53
54
|
visibleColumns: Array<Column>;
|
|
54
55
|
} & TableRowProps;
|
|
@@ -174,7 +175,8 @@ const Row = memo<RowProps>(
|
|
|
174
175
|
equals(prevProps.row, nextProps.row) &&
|
|
175
176
|
equals(previousRowConditions, nextRowConditions) &&
|
|
176
177
|
equals(previousRowColors, nextRowColors) &&
|
|
177
|
-
equals(prevProps.className, nextProps.className)
|
|
178
|
+
equals(prevProps.className, nextProps.className) &&
|
|
179
|
+
equals(prevProps.subItemsPivots, nextProps.subItemsPivots)
|
|
178
180
|
);
|
|
179
181
|
}
|
|
180
182
|
|
|
@@ -189,7 +191,8 @@ const Row = memo<RowProps>(
|
|
|
189
191
|
equals(prevIsShiftKeyDown, nextIsShiftKeyDown) &&
|
|
190
192
|
equals(prevShiftKeyDownRowPivot, nextShiftKeyDownRowPivot) &&
|
|
191
193
|
equals(prevLastSelectionIndex, nextLastSelectionIndex) &&
|
|
192
|
-
equals(prevViewMode, nextViewMode)
|
|
194
|
+
equals(prevViewMode, nextViewMode) &&
|
|
195
|
+
equals(prevProps.subItemsPivots, nextProps.subItemsPivots)
|
|
193
196
|
);
|
|
194
197
|
}
|
|
195
198
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
/* eslint-disable no-alert */
|
|
1
2
|
/* eslint-disable react/prop-types */
|
|
2
3
|
import { useState } from 'react';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
5
6
|
import { equals, prop } from 'ramda';
|
|
6
7
|
import { makeStyles } from 'tss-react/mui';
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ import { Column, ColumnType, SortOrder } from './models';
|
|
|
16
17
|
|
|
17
18
|
import Listing from '.';
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
const meta: Meta<typeof Listing> = {
|
|
20
21
|
argTypes: {
|
|
21
22
|
checkable: { control: 'boolean' },
|
|
22
23
|
currentPage: { control: 'number' },
|
|
@@ -26,7 +27,10 @@ export default {
|
|
|
26
27
|
},
|
|
27
28
|
component: Listing,
|
|
28
29
|
title: 'Listing'
|
|
29
|
-
}
|
|
30
|
+
};
|
|
31
|
+
export default meta;
|
|
32
|
+
|
|
33
|
+
type Story = StoryObj<typeof Listing>;
|
|
30
34
|
|
|
31
35
|
const useStyles = makeStyles()((theme) => ({
|
|
32
36
|
listing: {
|
|
@@ -48,6 +52,12 @@ const ComponentColumn = ({ row, isSelected }): JSX.Element => (
|
|
|
48
52
|
</>
|
|
49
53
|
);
|
|
50
54
|
|
|
55
|
+
const ButtonColumn = ({ row }): JSX.Element => (
|
|
56
|
+
<Button size="small" onClick={() => alert(JSON.stringify(row))}>
|
|
57
|
+
Click to reveal details about {row.name}
|
|
58
|
+
</Button>
|
|
59
|
+
);
|
|
60
|
+
|
|
51
61
|
const defaultColumns = [
|
|
52
62
|
{
|
|
53
63
|
getFormattedString: ({ name }): string => name,
|
|
@@ -131,7 +141,7 @@ const predefinedRowsSelection = [
|
|
|
131
141
|
}
|
|
132
142
|
];
|
|
133
143
|
|
|
134
|
-
const
|
|
144
|
+
const StoryTemplate = ({
|
|
135
145
|
columns = defaultColumns,
|
|
136
146
|
checkable = true,
|
|
137
147
|
viewerModeConfiguration,
|
|
@@ -164,7 +174,7 @@ const Story = ({
|
|
|
164
174
|
);
|
|
165
175
|
};
|
|
166
176
|
|
|
167
|
-
export const normal = (): JSX.Element => <
|
|
177
|
+
export const normal = (): JSX.Element => <StoryTemplate />;
|
|
168
178
|
|
|
169
179
|
export const WithSpecifiedViewMode = (): JSX.Element => {
|
|
170
180
|
const [viewMode, setViewMode] = useState(ListingVariant.extended);
|
|
@@ -173,7 +183,7 @@ export const WithSpecifiedViewMode = (): JSX.Element => {
|
|
|
173
183
|
: ListingVariant.compact;
|
|
174
184
|
|
|
175
185
|
return (
|
|
176
|
-
<
|
|
186
|
+
<StoryTemplate
|
|
177
187
|
viewMode={viewMode}
|
|
178
188
|
viewerModeConfiguration={{
|
|
179
189
|
onClick: () => setViewMode(newViewMode),
|
|
@@ -184,15 +194,15 @@ export const WithSpecifiedViewMode = (): JSX.Element => {
|
|
|
184
194
|
};
|
|
185
195
|
|
|
186
196
|
export const loadingWithNoData = (): JSX.Element => {
|
|
187
|
-
return <
|
|
197
|
+
return <StoryTemplate loading rows={[]} totalRows={0} />;
|
|
188
198
|
};
|
|
189
199
|
|
|
190
200
|
export const loadingWithData = (): JSX.Element => {
|
|
191
|
-
return <
|
|
201
|
+
return <StoryTemplate loading />;
|
|
192
202
|
};
|
|
193
203
|
|
|
194
204
|
export const asEmptyState = (): JSX.Element => {
|
|
195
|
-
return <
|
|
205
|
+
return <StoryTemplate rows={[]} />;
|
|
196
206
|
};
|
|
197
207
|
|
|
198
208
|
const actions = (
|
|
@@ -201,12 +211,16 @@ const actions = (
|
|
|
201
211
|
</Button>
|
|
202
212
|
);
|
|
203
213
|
|
|
204
|
-
export const withActions = (): JSX.Element =>
|
|
214
|
+
export const withActions = (): JSX.Element => (
|
|
215
|
+
<StoryTemplate actions={actions} />
|
|
216
|
+
);
|
|
205
217
|
|
|
206
|
-
export const withoutCheckboxes = (): JSX.Element =>
|
|
218
|
+
export const withoutCheckboxes = (): JSX.Element => (
|
|
219
|
+
<StoryTemplate checkable={false} />
|
|
220
|
+
);
|
|
207
221
|
|
|
208
222
|
export const withShortLabelColumns = (): JSX.Element => (
|
|
209
|
-
<
|
|
223
|
+
<StoryTemplate columns={columnsWithShortLabel} />
|
|
210
224
|
);
|
|
211
225
|
|
|
212
226
|
const editableColumns = [
|
|
@@ -270,7 +284,7 @@ const ListingWithEditableColumns = (): JSX.Element => {
|
|
|
270
284
|
};
|
|
271
285
|
|
|
272
286
|
return (
|
|
273
|
-
<
|
|
287
|
+
<StoryTemplate
|
|
274
288
|
columnConfiguration={{
|
|
275
289
|
selectedColumnIds,
|
|
276
290
|
sortable: true
|
|
@@ -290,23 +304,82 @@ export const withEditableAndSortableColumns = (): JSX.Element => (
|
|
|
290
304
|
<ListingWithEditableColumns />
|
|
291
305
|
);
|
|
292
306
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
columns
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
export const PlaygroundListing: Story = {
|
|
308
|
+
args: {
|
|
309
|
+
checkable: true,
|
|
310
|
+
columns: editableColumns,
|
|
311
|
+
currentPage: 1,
|
|
312
|
+
disableRowCheckCondition: (row): boolean => row.disableCheckbox,
|
|
313
|
+
disableRowCondition: (row): boolean => row.disableRow,
|
|
314
|
+
limit: 10,
|
|
315
|
+
loading: false,
|
|
316
|
+
predefinedRowsSelection,
|
|
317
|
+
rowColorConditions,
|
|
318
|
+
rows: listing,
|
|
319
|
+
totalRows: 10
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const listingWithSubItems = [...tenElements].map((_, index) => ({
|
|
324
|
+
active: false,
|
|
325
|
+
description: `Entity ${index}`,
|
|
326
|
+
disableCheckbox: false,
|
|
327
|
+
disableRow: false,
|
|
328
|
+
id: index,
|
|
329
|
+
name: `E${index}`,
|
|
330
|
+
selected: false,
|
|
331
|
+
subItems:
|
|
332
|
+
index % 2 === 0
|
|
333
|
+
? [...tenElements].map((__, subIndex) => ({
|
|
334
|
+
active: false,
|
|
335
|
+
description: `Sub item ${subIndex + (index + 10) * 10} description`,
|
|
336
|
+
disableCheckbox: false,
|
|
337
|
+
disableRow: false,
|
|
338
|
+
id: subIndex + (index + 10) * 10,
|
|
339
|
+
name: `Sub Item ${subIndex + (index + 10) * 10}`,
|
|
340
|
+
selected: false
|
|
341
|
+
}))
|
|
342
|
+
: undefined
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const columnsWithSubItems = [
|
|
346
|
+
{
|
|
347
|
+
getFormattedString: ({ name }): string => name,
|
|
348
|
+
id: 'name',
|
|
349
|
+
label: 'Name',
|
|
350
|
+
type: ColumnType.string
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
getFormattedString: ({ description }): string => description,
|
|
354
|
+
id: 'description',
|
|
355
|
+
label: 'Description',
|
|
356
|
+
type: ColumnType.string
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
Component: ButtonColumn,
|
|
360
|
+
displaySubItemsCaret: true,
|
|
361
|
+
id: '#',
|
|
362
|
+
label: 'Custom',
|
|
363
|
+
type: ColumnType.component,
|
|
364
|
+
width: '350px'
|
|
365
|
+
}
|
|
366
|
+
];
|
|
304
367
|
|
|
305
|
-
export const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
368
|
+
export const ListingWithSubItems = {
|
|
369
|
+
args: {
|
|
370
|
+
checkable: true,
|
|
371
|
+
columns: columnsWithSubItems,
|
|
372
|
+
currentPage: 1,
|
|
373
|
+
limit: 10,
|
|
374
|
+
loading: false,
|
|
375
|
+
rows: listingWithSubItems,
|
|
376
|
+
subItems: {
|
|
377
|
+
canCheckSubItems: false,
|
|
378
|
+
enable: true,
|
|
379
|
+
labelCollapse: 'Collapse',
|
|
380
|
+
labelExpand: 'Expand',
|
|
381
|
+
rowProperty: 'subItems'
|
|
382
|
+
},
|
|
383
|
+
totalRows: 10
|
|
384
|
+
}
|
|
312
385
|
};
|
package/src/Listing/index.tsx
CHANGED
|
@@ -18,14 +18,17 @@ import {
|
|
|
18
18
|
map,
|
|
19
19
|
not,
|
|
20
20
|
pick,
|
|
21
|
+
pluck,
|
|
21
22
|
prop,
|
|
22
23
|
propEq,
|
|
24
|
+
reduce,
|
|
23
25
|
reject,
|
|
24
26
|
slice,
|
|
25
27
|
subtract,
|
|
26
28
|
uniqBy
|
|
27
29
|
} from 'ramda';
|
|
28
30
|
import { useTranslation } from 'react-i18next';
|
|
31
|
+
import { useAtomValue } from 'jotai';
|
|
29
32
|
|
|
30
33
|
import { Box, LinearProgress, Table, TableBody } from '@mui/material';
|
|
31
34
|
|
|
@@ -50,10 +53,11 @@ import ListingRow from './Row/Row';
|
|
|
50
53
|
import { labelNoResultFound } from './translatedLabels';
|
|
51
54
|
import useResizeObserver from './useResizeObserver';
|
|
52
55
|
import useStyleTable from './useStyleTable';
|
|
53
|
-
import { loadingIndicatorHeight,
|
|
56
|
+
import { loadingIndicatorHeight, useListingStyles } from './Listing.styles';
|
|
54
57
|
import { EmptyResult } from './EmptyResult/EmptyResult';
|
|
55
58
|
import { SkeletonLoader } from './Row/SkeletonLoaderRows';
|
|
56
59
|
import { ListingHeader } from './Header';
|
|
60
|
+
import { subItemsPivotsAtom } from './tableAtoms';
|
|
57
61
|
|
|
58
62
|
const getVisibleColumns = ({
|
|
59
63
|
columnConfiguration,
|
|
@@ -114,6 +118,13 @@ export interface Props<TRow> {
|
|
|
114
118
|
selectedRows?: Array<TRow>;
|
|
115
119
|
sortField?: string;
|
|
116
120
|
sortOrder?: SortOrder;
|
|
121
|
+
subItems?: {
|
|
122
|
+
canCheckSubItems: boolean;
|
|
123
|
+
enable: boolean;
|
|
124
|
+
labelCollapse: string;
|
|
125
|
+
labelExpand: string;
|
|
126
|
+
rowProperty: string;
|
|
127
|
+
};
|
|
117
128
|
totalRows?: number;
|
|
118
129
|
viewMode?: ListingVariant;
|
|
119
130
|
viewerModeConfiguration?: ViewerModeConfiguration;
|
|
@@ -160,7 +171,14 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
160
171
|
viewMode = ListingVariant.compact,
|
|
161
172
|
widthToMoveTablePagination,
|
|
162
173
|
getHighlightRowCondition,
|
|
163
|
-
viewerModeConfiguration
|
|
174
|
+
viewerModeConfiguration,
|
|
175
|
+
subItems = {
|
|
176
|
+
canCheckSubItems: false,
|
|
177
|
+
enable: false,
|
|
178
|
+
labelCollapse: 'Collapse',
|
|
179
|
+
labelExpand: 'Expand',
|
|
180
|
+
rowProperty: ''
|
|
181
|
+
}
|
|
164
182
|
}: Props<TRow>): JSX.Element => {
|
|
165
183
|
const currentVisibleColumns = getVisibleColumns({
|
|
166
184
|
columnConfiguration,
|
|
@@ -172,13 +190,6 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
172
190
|
viewMode
|
|
173
191
|
});
|
|
174
192
|
|
|
175
|
-
const { classes, theme } = useStyles({
|
|
176
|
-
dataStyle,
|
|
177
|
-
getGridTemplateColumn,
|
|
178
|
-
limit,
|
|
179
|
-
rows,
|
|
180
|
-
viewMode
|
|
181
|
-
});
|
|
182
193
|
const { t } = useTranslation();
|
|
183
194
|
|
|
184
195
|
const [tableTopOffset, setTableTopOffset] = React.useState(0);
|
|
@@ -192,6 +203,37 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
192
203
|
const containerRef = React.useRef<HTMLDivElement>();
|
|
193
204
|
const actionBarRef = React.useRef<HTMLDivElement>();
|
|
194
205
|
|
|
206
|
+
const subItemsPivots = useAtomValue(subItemsPivotsAtom);
|
|
207
|
+
|
|
208
|
+
const rowsToDisplay = React.useMemo(
|
|
209
|
+
() =>
|
|
210
|
+
subItems?.enable
|
|
211
|
+
? reduce<TRow, Array<TRow>>(
|
|
212
|
+
(acc, row): Array<TRow> => {
|
|
213
|
+
if (
|
|
214
|
+
row[subItems.rowProperty] &&
|
|
215
|
+
subItemsPivots.includes(row.id)
|
|
216
|
+
) {
|
|
217
|
+
return [...acc, row, ...row[subItems.rowProperty]];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return [...acc, row];
|
|
221
|
+
},
|
|
222
|
+
[],
|
|
223
|
+
rows
|
|
224
|
+
)
|
|
225
|
+
: rows,
|
|
226
|
+
[rows, subItemsPivots, subItems]
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const { classes, theme } = useListingStyles({
|
|
230
|
+
dataStyle,
|
|
231
|
+
getGridTemplateColumn,
|
|
232
|
+
limit,
|
|
233
|
+
rows: rowsToDisplay,
|
|
234
|
+
viewMode
|
|
235
|
+
});
|
|
236
|
+
|
|
195
237
|
useResizeObserver({
|
|
196
238
|
onResize: () => {
|
|
197
239
|
setTableTopOffset(getCumulativeOffset(containerRef.current));
|
|
@@ -422,6 +464,19 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
422
464
|
|
|
423
465
|
const areColumnsEditable = not(isNil(onSelectColumns));
|
|
424
466
|
|
|
467
|
+
const allSubItemIds = React.useMemo(
|
|
468
|
+
() =>
|
|
469
|
+
reduce<TRow | number, Array<string | number>>(
|
|
470
|
+
(acc, row) => [
|
|
471
|
+
...acc,
|
|
472
|
+
...pluck('id', row[subItems?.rowProperty || ''] || [])
|
|
473
|
+
],
|
|
474
|
+
[],
|
|
475
|
+
rows
|
|
476
|
+
),
|
|
477
|
+
[rows, subItems]
|
|
478
|
+
);
|
|
479
|
+
|
|
425
480
|
return (
|
|
426
481
|
<>
|
|
427
482
|
{loading && rows.length > 0 && (
|
|
@@ -495,14 +550,16 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
495
550
|
component="div"
|
|
496
551
|
onMouseLeave={clearHoveredRow}
|
|
497
552
|
>
|
|
498
|
-
{
|
|
553
|
+
{rowsToDisplay.map((row, index) => {
|
|
499
554
|
const isRowSelected = isSelected(row);
|
|
500
|
-
|
|
501
555
|
const isRowHovered = equals(hoveredRowId, getId(row));
|
|
556
|
+
const isSubItem = allSubItemIds.includes(row.id);
|
|
502
557
|
|
|
503
558
|
return (
|
|
504
559
|
<ListingRow
|
|
505
|
-
checkable={
|
|
560
|
+
checkable={
|
|
561
|
+
checkable && (!isSubItem || subItems.canCheckSubItems)
|
|
562
|
+
}
|
|
506
563
|
columnConfiguration={columnConfiguration}
|
|
507
564
|
columnIds={columns.map(prop('id'))}
|
|
508
565
|
disableRowCondition={disableRowCondition}
|
|
@@ -519,6 +576,7 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
519
576
|
row={row}
|
|
520
577
|
rowColorConditions={rowColorConditions}
|
|
521
578
|
shiftKeyDownRowPivot={shiftKeyDownRowPivot}
|
|
579
|
+
subItemsPivots={subItemsPivots}
|
|
522
580
|
tabIndex={-1}
|
|
523
581
|
viewMode={viewMode}
|
|
524
582
|
visibleColumns={visibleColumns}
|
|
@@ -528,28 +586,37 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
528
586
|
onFocus={(): void => hoverRow(row)}
|
|
529
587
|
onMouseOver={(): void => hoverRow(row)}
|
|
530
588
|
>
|
|
531
|
-
{checkable &&
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
589
|
+
{checkable &&
|
|
590
|
+
(!isSubItem || subItems.canCheckSubItems ? (
|
|
591
|
+
<Cell
|
|
592
|
+
align="left"
|
|
593
|
+
className={classes.checkbox}
|
|
594
|
+
disableRowCondition={disableRowCondition}
|
|
595
|
+
isRowHovered={isRowHovered}
|
|
596
|
+
row={row}
|
|
597
|
+
rowColorConditions={rowColorConditions}
|
|
598
|
+
onClick={(event): void => selectRow(event, row)}
|
|
599
|
+
>
|
|
600
|
+
<Checkbox
|
|
601
|
+
checked={isRowSelected}
|
|
602
|
+
disabled={
|
|
603
|
+
disableRowCheckCondition(row) ||
|
|
604
|
+
disableRowCondition(row)
|
|
605
|
+
}
|
|
606
|
+
inputProps={{
|
|
607
|
+
'aria-label': `Select row ${getId(row)}`
|
|
608
|
+
}}
|
|
609
|
+
/>
|
|
610
|
+
</Cell>
|
|
611
|
+
) : (
|
|
612
|
+
<Cell
|
|
613
|
+
align="left"
|
|
614
|
+
disableRowCondition={disableRowCondition}
|
|
615
|
+
isRowHovered={isRowHovered}
|
|
616
|
+
row={row}
|
|
617
|
+
rowColorConditions={rowColorConditions}
|
|
550
618
|
/>
|
|
551
|
-
|
|
552
|
-
)}
|
|
619
|
+
))}
|
|
553
620
|
|
|
554
621
|
{visibleColumns.map((column) => (
|
|
555
622
|
<DataCell
|
|
@@ -559,8 +626,11 @@ const Listing = <TRow extends { id: RowId }>({
|
|
|
559
626
|
isRowHovered={isRowHovered}
|
|
560
627
|
isRowSelected={isRowSelected}
|
|
561
628
|
key={`${getId(row)}-${column.id}`}
|
|
629
|
+
labelCollapse={subItems.labelCollapse}
|
|
630
|
+
labelExpand={subItems.labelExpand}
|
|
562
631
|
row={row}
|
|
563
632
|
rowColorConditions={rowColorConditions}
|
|
633
|
+
subItemsRowProperty={subItems?.rowProperty}
|
|
564
634
|
viewMode={viewMode}
|
|
565
635
|
/>
|
|
566
636
|
))}
|
package/src/Listing/models.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Column {
|
|
|
19
19
|
compact?: boolean;
|
|
20
20
|
disablePadding?: boolean;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
displaySubItemsCaret?: boolean;
|
|
22
23
|
getColSpan?: (isSelected) => number | undefined;
|
|
23
24
|
getFormattedString?: (row) => string | null;
|
|
24
25
|
getHiddenCondition?: (isSelected) => boolean;
|