@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "23.10.40",
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 Cell = (props: Props): JSX.Element => {
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 useStyles = makeStyles<StylesProps>()(
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 { useStyles, loadingIndicatorHeight };
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
+ ```
@@ -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 { ComponentMeta, ComponentStory } from '@storybook/react';
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
- export default {
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
- } as ComponentMeta<typeof Listing>;
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 Story = ({
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 => <Story />;
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
- <Story
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 <Story loading rows={[]} totalRows={0} />;
197
+ return <StoryTemplate loading rows={[]} totalRows={0} />;
188
198
  };
189
199
 
190
200
  export const loadingWithData = (): JSX.Element => {
191
- return <Story loading />;
201
+ return <StoryTemplate loading />;
192
202
  };
193
203
 
194
204
  export const asEmptyState = (): JSX.Element => {
195
- return <Story rows={[]} />;
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 => <Story actions={actions} />;
214
+ export const withActions = (): JSX.Element => (
215
+ <StoryTemplate actions={actions} />
216
+ );
205
217
 
206
- export const withoutCheckboxes = (): JSX.Element => <Story checkable={false} />;
218
+ export const withoutCheckboxes = (): JSX.Element => (
219
+ <StoryTemplate checkable={false} />
220
+ );
207
221
 
208
222
  export const withShortLabelColumns = (): JSX.Element => (
209
- <Story columns={columnsWithShortLabel} />
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
- <Story
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 TemplateListing: ComponentStory<typeof Listing> = (args) => (
294
- <Listing
295
- {...args}
296
- columns={editableColumns}
297
- disableRowCheckCondition={(row): boolean => row.disableCheckbox}
298
- disableRowCondition={(row): boolean => row.disableRow}
299
- predefinedRowsSelection={predefinedRowsSelection}
300
- rowColorConditions={rowColorConditions}
301
- rows={listing}
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 PlaygroundListing = TemplateListing.bind({});
306
- PlaygroundListing.args = {
307
- checkable: true,
308
- currentPage: 1,
309
- limit: 10,
310
- loading: false,
311
- totalRows: 10
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
  };
@@ -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, useStyles } from './Listing.styles';
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
- {rows.map((row, index) => {
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={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
- <Cell
533
- align="left"
534
- className={classes.checkbox}
535
- disableRowCondition={disableRowCondition}
536
- isRowHovered={isRowHovered}
537
- row={row}
538
- rowColorConditions={rowColorConditions}
539
- onClick={(event): void => selectRow(event, row)}
540
- >
541
- <Checkbox
542
- checked={isRowSelected}
543
- disabled={
544
- disableRowCheckCondition(row) ||
545
- disableRowCondition(row)
546
- }
547
- inputProps={{
548
- 'aria-label': `Select row ${getId(row)}`
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
- </Cell>
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
  ))}
@@ -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;
@@ -43,3 +43,5 @@ export const tableStyleDerivedAtom = atom(null, (get, set, { viewMode }) => {
43
43
  header: isExtendedMode ? extendedTableHeader : compactTableHeader
44
44
  });
45
45
  });
46
+
47
+ export const subItemsPivotsAtom = atom<Array<number | string>>([]);
@@ -86,7 +86,7 @@ export interface Tab {
86
86
 
87
87
  export interface Props {
88
88
  className?: string;
89
- header: React.ReactElement;
89
+ header?: React.ReactElement;
90
90
  headerBackgroundColor?: string;
91
91
  labelClose?: string;
92
92
  minWidth?: number;