Skip to content

Support for grouping in ItemTable #177

Open
doug opened this issue Feb 3, 2021 · 13 comments
Open

Support for grouping in ItemTable #177

doug opened this issue Feb 3, 2021 · 13 comments
Assignees
Labels
feature-request Request for functionality that has not already been implemented handoff Issues that will not be fixed by the first webqueue2 team (after 7/16/21)

Comments

@doug
Copy link

doug commented Feb 3, 2021

I use the group by priority in the current webqueue and would like to see a way for that to implemented in this version. I need some sort of break between priorities to distinguish when they change.

image

@doug doug added the feature-request Request for functionality that has not already been implemented label Feb 3, 2021
@harley
Copy link

harley commented Feb 3, 2021

I would add group by any of the column headers.

@campb303 campb303 changed the title Ability to group by priority Support for grouping in ItemTable Feb 5, 2021
@campb303 campb303 added this to the v2-production-ready-read-only milestone Feb 5, 2021
@campb303
Copy link
Collaborator

campb303 commented Feb 5, 2021

This functionality is supported in react-table and needs to be exposed via the UI.

@campb303 campb303 removed the frontend label Mar 17, 2021
@campb303 campb303 removed this from the v2-production-ready-read-only milestone Mar 29, 2021
@wrigh393
Copy link
Collaborator

wrigh393 commented Jun 9, 2021

Work has begun on this issue. Here are a few notes on how grouping works in react-table and the changes implementing it may require.

  • react-table handles grouping by collapsing all the rows of the grouped category into one row. This would require us to make changes to both how we open items. Currently, items are open when a user clicks anywhere on a row so we would need to alter that so users could expand the collapsed rows that grouping creates.
    • There may be a way to group and not collapse the rows but I would need to do further research.
  • We also need to look at the best way to implement this from a UI standpoint. This could be in the form of an AutoComplete component similar to the one used for the QueueSelector

@wrigh393
Copy link
Collaborator

wrigh393 commented Jun 10, 2021

Here is a gif to give a visual on how react-table handles grouping:

Animated GIF-downsized_large

Also here is a link to this example on codesandbox

@wrigh393
Copy link
Collaborator

wrigh393 commented Jun 10, 2021

I was able to get a rough implementation of grouping working. Below I will detail what needed to be added for this to work and what still needs to be done/improved

Implementation

First, in order to use grouping in react-table we need to add the the required hooks to the tableInstance variable:

  • useGroupBy
  • useExpanded.
    I will show where these are added. Note that they HAVE to be in the order that they are in below. If you do not put them in this specific order react-table will give an error message. See this issue and PR from react-table to see why this is necessary (issue 1823, PR 2581),
   const tableInstance = useTable(
        {
            columns,
            data,
            autoResetSortBy: false,
            autoResetFilters: false,
            defaultColumn: {
                Filter: ({ column: { Header, setFilter } }) => {
                    return (
                        <ItemTableFilter
                            label={Header}
                            onChange={(event) => setFilter(event.target.value)}
                        />
                    );
                },
            },
            initialState: {
                sortBy: [
                    { id: "queue" },
                    { id: 'number' },
                    { id: 'lastUpdated' },
                ],
            },
        },
-        useFilters, useFlexLayout, useSortBy,
+       useFilters, useFlexLayout, useGroupBy, useSortBy, useExpanded
    );

Next, aggregates were added to each column. To add aggregates to a column there are two props to pass, aggregate which determines how react-table aggregates the info from the rows, and Aggregated which determines the values shown when a cell is aggregated.
Here is a list of values that can be passed to aggregate by default. Note that you can also pass a user-defined function to handle aggregation.

  • count: Finds the total number of value in that group
  • uniqueCount: finds the number of unique values in that group.
  • sum: adds the sum of all the values for that group of data
  • average: Finds the average of all values for that group of data

Here is how this implementation looks in our code:

 const columns = React.useMemo(
        () => [
            { Header: 'Queue', accessor: 'queue',aggregate: 'uniqueCount', Aggregated: ({ value }) => `In ${value} queue(s) `, },
            { Header: 'Item #', accessor: 'number', aggregate: 'count', Aggregated: ({ value }) => `${value} items in queue` },
            { Header: 'From', accessor: 'userAlias', aggregate: 'uniqueCount', Aggregated: ({ value }) => `Items from ${value} unique user(s) ` },
            { Header: 'Assigned To', accessor: 'assignedTo', aggregate: 'uniqueCount', Aggregated: ({ value }) => `Items assigned to ${value} unique staff memeber(s) ` },
            { Header: 'Subject', accessor: 'subject', aggregate: 'uniqueCount', Aggregated: ({ value }) => `${value} unique subject(s)` },
            { Header: 'Status', accessor: 'status', aggregate: 'uniqueCount', Aggregated: ({ value }) => `${value} unique status(es)` },
            { Header: 'Priority', accessor: 'priority', aggregate: 'uniqueCount', Aggregated: ({ value }) => `${value} unique priorities` },
            { Header: 'Last Updated', accessor: 'lastUpdated', aggregate: 'sum', Aggregated: ({ value }) => `${Math.round(value * 100) / 100} (avg) time since last update`, sortInverted: true, Cell: ({ value }) => <RelativeTime value={value} /> },
            { Header: 'Department', accessor: 'department', aggregate: 'uniqueCount', Aggregated: ({ value }) => `${value} unique department(s)` },
            { Header: 'Building', accessor: 'building', aggregate: 'uniqueCount', Aggregated: ({ value }) => `${value} unique building(s)` },
            { Header: 'Date Received', accessor: 'dateReceived', aggregate: 'sum', Aggregated: ({ value }) => `${Math.round(value * 100) / 100} (avg) date received`, sortInverted: true, Cell: ({ value }) => <RelativeTime value={value} /> },
        ], []);

Next, a rough UI for interacting with grouping was implemented. In order to interact with grouping, we need to pass the getGroupByToggleProps to a component. Because this is a column prop we need to expand the column props and then pass the getGroupByToggleProps. For the UI, I added a TableRow component to the table header, and inside that row is a grid with an IconButton used to toggle grouping and the header of the column.

//Maps this UI to each column header
 {headerGroup.headers.map(column => (
 <TableRow {...column.getHeaderProps()}>
     <Grid container spacing={0}>
         <Grid item>
             {column.canGroupBy ? (
                  // expand column props. then pass getGroupByToggleProps
                  <IconButton size="small" justify="center" {...column.getGroupByToggleProps()}>
                        //Change icon based on if column is grouped or not
                       {column.isGrouped ? <LayersClear /> : <Layers />}
                  </IconButton>
               ) : null}
         </Grid>
      <Grid item>
          {column.render('Header')}
       </Grid>
     </Grid>
 </TableRow>
 ))}

Screenshot of the above UI
image
*Note: This is not the final UI

Next the UI for the table needed to be addressed. react-table handles grouping by collapsing the grouped rows into a single row. The grouped information can then be seen by expanding the row (which is why we needed to add the useExpanded hook in the first step). Here are the changes to the row cell renders that allow for expansion of the rows, and show the aggregated value of the cells when aggregated.

{row.cells.map(cell => {
+    return ( 
+                  cell.isGrouped ? (
+                         // If it's a grouped cell, add an expander and row count
+                        <ItemTableCell TableCellProps={cell.getCellProps()}>
+                            <IconButton size="small" {...row.getToggleRowExpandedProps()}>
+                                {row.isExpanded ? <ArrowDropDown /> : <ArrowRight />}
+                            </IconButton>
+                             {cell.render('Cell')} ({row.subRows.length})
+                          </ItemTableCell> ) : 
+                  cell.isAggregated ? (
+                       // If the cell is aggregated, use the Aggregated renderer for cell
+                      <ItemTableCell TableCellProps={cell.getCellProps()}>
+                           {cell.render('Aggregated')}
+                       </ItemTableCell>  ) : 
+                  //If the cell is a placeholder don't render anything
+                  cell.isPlaceholder ? null : 
+                  ( 
                       // Otherwise, just render the regular cell
                       cell.render(_ => {
                           switch (cell.column.id) {
                               case "dateReceived":
                                   return (
                                       <ItemTableCell TableCellProps={cell.getCellProps()}>
                                            <RelativeTime value={cell.value} />
                                       </ItemTableCell>
                                   );
                                case "lastUpdated":
                                    return (
                                        <LastUpdatedCell
                                             time={cell.value}
                                             ItemTableCellProps={cell.getCellProps()}
                                         />
                                    );
                                default:
                                    return (
                                        <ItemTableCell TableCellProps={cell.getCellProps()}>
                                            {cell.value}
                                       </ItemTableCell>
                                    );
                 }
            })
        )
+    )
})}

Next Steps

There are a few things that still need to happen:

  • The function that is used to open items needs to be rewritten to take into account if a row has been grouped
  • The UI needs to be refined
  • Aggregate values need to be looked at for clarity
  • A custom function may need to be created for aggregation of the LastUpdated and DateReceieved columns

@campb303
Copy link
Collaborator

Note that they HAVE to be in the order that they are in below.

Why is this?

To add aggregates to a column there are two props to pass...

If something can be a list, it should be a list.

For the UI, I added a TableRow component to the table header, and inside that row is a grid with an IconButton used to toggle grouping and the header of the column.

When talking about UI, include screenshot.

Be sure to properly format code snippets.

<Grid container spacing={0}>
         <Grid item>
             {column.canGroupBy ? (
                  // expand column props. then pass getGroupByToggleProps
                  <IconButton size="small" justify="center" {...column.getGroupByToggleProps()}>
                        //Change icon based on if column is grouped or not
                       {column.isGrouped ? <LayersClear /> : <Layers />}
                  </IconButton>
               ) : null}
         </Grid>
      <Grid item>.   // This line is improperly indented.
          {column.render('Header')}
       </Grid>

which is why we needed to add the useExpanded hook

Can this hook be modified to be open by default?

@wrigh393
Copy link
Collaborator

My initial comment detailing the rough implementation of groping has been updated to include more information. Also important to note is that useExpanded cannot be set to be open by default through existing react-table methods but this issue from the react-table GitHub offers solutions that may work.

@wrigh393
Copy link
Collaborator

I have been unable to get the solutions suggested in my previous comment to work in our table. In theory, we should be able to use a useEffect hook to set all rows to be expanded by default, but this does not seem to be the case.

 useEffect(() => {
        toggleAllRowsExpanded(true);
    }, [isAllRowsExpanded]);

It seems that the toggleAllRowsExpanded prop available in react-table is not actually setting the rows to be expanded. I was able to toggle all rows to be expanded using the getToggleAllRowsExpandedProps prop and passing that to a UI element but this wouldn't solve the issue of them being expanded by default. Further work needs to be done to understand why toggleAllRowExpanded does not work.

I also need to look into how we can display grouped information in a way similar to the current webqueue. Here is how it looks in the current webqueue vs webqueue2.

Current
image

webqueue2 groups collapsed
image

webqueue2 groups expanded
image

This is important because we want to keep the workflow as close to the existing one as possible. Further research is needed to seehow custom rendering of groups in react-table works.

@wrigh393
Copy link
Collaborator

To test if the toggleAllRowsExpanded function provided works with our code I added a button that runs the function on click. This button worked as expected so my next step is to look into the proper way to get the function to run on the first render as the useEffect used in the above comment is not working.

@wrigh393
Copy link
Collaborator

Here is an update on the progress I've made so far.

Default expanded view

Instead of trying to set the state of the isExpanded variable on the first render, I had the idea to set the state when grouping occurs.

When you group rows by a specific value those rows are grouped into rows based on that value that contains sub rows. Each row has a prop, canExpand which determines if a row can expand or not. For the rows containing the sub rows, this prop is set to true. The solution I implemented uses the toggleRowExpanded function to set each row that can expand to be expanded. Since this only happens when rows have grouped this fires every time that happens. Below is the code for this implementation.

   row.canExpand ? row.toggleRowExpanded(true) : row.toggleRowExpanded(false)

This still needs work as right now the grouped rows can't be collapsed.

isSelected fix

Due to how react-table grouped row objects are structured we were running into an issue with our isSelected variable now being able to read the correct props of a grouped row. isSelected was being set using the row.original to access both the queue and item number from that prop. When rows are grouped in react-table the resulting grouped row does not have a row prop. to address this I changed isSelected to only be set when an item is not grouped. Here is the code for that change.

- let isSelected = selectedRow.queue === row.original.queue && selectedRow.number === row.original.number
+ let isSelected = undefined
+ const handleIsSelected = !row.isGrouped ? isSelected = selectedRow.queue === row.original.queue && selectedRow.number === row.original.number : false

This works fine but refactoring is needed.

Next Steps

  • Fix expansion so that rows can be collapsed
  • Refactor isSelected fix
  • Look into a better way to implement this UI
  • Clarify aggregate values

@wrigh393
Copy link
Collaborator

In order to keep the workflow as close to the existing one as possible, I implemented a Select component for the grouping of items. The component uses a function that takes advantage the reduce() method to simplify the column object that is passed into one value. This is used to flatten the nested information that is received from the columns in react-table. This in turn makes it easier to access the props that we need to access for the drop down. Here is the function:

    const getLeafColumns = (rootColumns) => {
        return rootColumns.reduce((leafColumns, column) => {
            if (column.columns) {
                return [...leafColumns, ...getLeafColumns(column.columns)] ;
            } else {
                return [...leafColumns, column];
            }
        }, []);
    }

Next is the code for the UI. It takes advantage of these components from Material-UI:

  • Paper: this is mostly for proper spacing
  • FormControl: this provides context for form inputs like the select used in this component.
  • InputLabel: This is the label for the select input
  • Select: This behaves the same as a select HTML element but has styling consistent with the rest of the UI
  • MenuItem: These behave like the option HTML element which is used to populate the select menu with options.

Here is how these component come together in code and how they appear in the UI:

GroupBySelector component

<Paper elevation={0} classes={{ root: classes.Paper_root }}>
            <FormControl classes={{ root: classes.formControl_root }} variant="outlined" size="small">
                <InputLabel id="Group-By-Select">Group By</InputLabel>
                <Select
                    classes={{ root: classes.Select_root }}
                    value={value}
                    onChange={onChange}
                    label="Group By"
                    autoWidth
                >
                    <MenuItem value="">None</MenuItem>
                    {getLeafColumns(tableColumns).map(column => (
                        <MenuItem key={column.id} value={column.id}>{column.Header}</MenuItem>
                    ))}
                </Select>
            </FormControl>
        </Paper>

ItemTable component

 <TableContainer>
+    <GroupBySelector
+        value={groupBy[0]}
+        onChange={e => { setGroupBy([e.target.value]) }}
+        tableColumns={allColumns}
+    />
    <Table stickyHeader {...getTableProps()} size="small">
///The rest of the code...

UI Screenshots

Selector closed:
image

Selector open:
image

@campb303
Copy link
Collaborator

With some extra UI code from us, we might be able to acheive the same view as the existing webqueue for grouping with pivotBy as suggested in this comment.

@campb303 campb303 assigned campb303 and unassigned wrigh393 Jul 6, 2021
@campb303 campb303 added the handoff Issues that will not be fixed by the first webqueue2 team (after 7/16/21) label Jul 8, 2021
@campb303
Copy link
Collaborator

campb303 commented Jul 8, 2021

This will not be completed by the current team. At present the useGroupBy plugin for react-table can functionally achieve this goal but its default UI behaviors cause columns to be reordered in ways that break the usability of the table. In addition to this, there is not time to develop a proper UI. Mobile access to a grouped table is also a concern.

To the next person who looks at this issue, see these resources in this order:

  • The first commet in this issue demonstrates the desired output.
  • The Getting Started section of react-table docs has an Overview and QuickStart section that help understand the basics.
  • The useGroupBy Plugin docs show you how to use the plugin with code examples.

Sign in to join this conversation on GitHub.
Labels
feature-request Request for functionality that has not already been implemented handoff Issues that will not be fixed by the first webqueue2 team (after 7/16/21)
Projects
None yet
Development

No branches or pull requests

4 participants