UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

318 lines (306 loc) 10.6 kB
import { useCallback, useMemo } from 'react'; import * as aggregations from '../aggregations.js'; import { actions, makePropGetter, ensurePluginOrder, useMountedLayoutEffect, useGetLatest } from '../publicUtils.js'; import { getFirstDefined, flattenBy } from '../utils.js'; const emptyArray = []; const emptyObject = {}; // Actions actions.resetGroupBy = 'resetGroupBy'; actions.setGroupBy = 'setGroupBy'; actions.toggleGroupBy = 'toggleGroupBy'; export const useGroupBy = hooks => { hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps]; hooks.stateReducers.push(reducer); hooks.visibleColumnsDeps.push((deps, { instance }) => [...deps, instance.state.groupBy]); hooks.visibleColumns.push(visibleColumns); hooks.useInstance.push(useInstance); hooks.prepareRow.push(prepareRow); }; useGroupBy.pluginName = 'useGroupBy'; const defaultGetGroupByToggleProps = (props, { header }) => [props, { onClick: header.canGroupBy ? e => { e.persist(); header.toggleGroupBy(); } : undefined, style: { cursor: header.canGroupBy ? 'pointer' : undefined }, title: 'Toggle GroupBy' }]; function reducer(state, action, _previousState, instance) { if (action.type === actions.init) { return { groupBy: [], ...state }; } if (action.type === actions.resetGroupBy) { return { ...state, groupBy: instance.initialState.groupBy || [] }; } if (action.type === actions.setGroupBy) { const { value } = action; return { ...state, groupBy: value }; } if (action.type === actions.toggleGroupBy) { const { columnId, value: setGroupBy } = action; const resolvedGroupBy = typeof setGroupBy !== 'undefined' ? setGroupBy : !state.groupBy.includes(columnId); if (resolvedGroupBy) { return { ...state, groupBy: [...state.groupBy, columnId] }; } return { ...state, groupBy: state.groupBy.filter(d => d !== columnId) }; } } function visibleColumns(columns, { instance: { state: { groupBy } } }) { // Sort grouped columns to the start of the column list // before the headers are built const groupByColumns = groupBy.map(g => columns.find(col => col.id === g)).filter(Boolean); const nonGroupByColumns = columns.filter(col => !groupBy.includes(col.id)); columns = [...groupByColumns, ...nonGroupByColumns]; columns.forEach(column => { column.isGrouped = groupBy.includes(column.id); column.groupedIndex = groupBy.indexOf(column.id); }); return columns; } const defaultUserAggregations = {}; function useInstance(instance) { const { data, rows, flatRows, rowsById, allColumns, flatHeaders, groupByFn = defaultGroupByFn, manualGroupBy, aggregations: userAggregations = defaultUserAggregations, plugins, state: { groupBy }, dispatch, autoResetGroupBy = true, disableGroupBy, defaultCanGroupBy, getHooks } = instance; ensurePluginOrder(plugins, ['useColumnOrder', 'useFilters'], 'useGroupBy'); const getInstance = useGetLatest(instance); allColumns.forEach(column => { const { accessor, defaultGroupBy: defaultColumnGroupBy, disableGroupBy: columnDisableGroupBy } = column; column.canGroupBy = accessor ? getFirstDefined(column.canGroupBy, columnDisableGroupBy === true ? false : undefined, disableGroupBy === true ? false : undefined, true) : getFirstDefined(column.canGroupBy, defaultColumnGroupBy, defaultCanGroupBy, false); if (column.canGroupBy) { column.toggleGroupBy = () => instance.toggleGroupBy(column.id); } column.Aggregated = column.Aggregated || column.Cell; }); const toggleGroupBy = useCallback((columnId, value) => { dispatch({ type: actions.toggleGroupBy, columnId, value }); }, [dispatch]); const setGroupBy = useCallback(value => { dispatch({ type: actions.setGroupBy, value }); }, [dispatch]); flatHeaders.forEach(header => { header.getGroupByToggleProps = makePropGetter(getHooks().getGroupByToggleProps, { instance: getInstance(), header }); }); const [groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById] = useMemo(() => { if (manualGroupBy || !groupBy.length) { return [rows, flatRows, rowsById, emptyArray, emptyObject, flatRows, rowsById]; } // Ensure that the list of filtered columns exist const existingGroupBy = groupBy.filter(g => allColumns.find(col => col.id === g)); // Find the columns that can or are aggregating // Uses each column to aggregate rows into a single value const aggregateRowsToValues = (leafRows, groupedRows, depth) => { const values = {}; allColumns.forEach(column => { // Don't aggregate columns that are in the groupBy if (existingGroupBy.includes(column.id)) { values[column.id] = groupedRows[0] ? groupedRows[0].values[column.id] : null; return; } // Aggregate the values const aggregateFn = typeof column.aggregate === 'function' ? column.aggregate : userAggregations[column.aggregate] || aggregations[column.aggregate]; if (aggregateFn) { // Get the columnValues to aggregate const groupedValues = groupedRows.map(row => row.values[column.id]); // Get the columnValues to aggregate const leafValues = leafRows.map(row => { let columnValue = row.values[column.id]; if (!depth && column.aggregateValue) { const aggregateValueFn = typeof column.aggregateValue === 'function' ? column.aggregateValue : userAggregations[column.aggregateValue] || aggregations[column.aggregateValue]; if (!aggregateValueFn) { console.info({ column }); throw new Error(`React Table: Invalid column.aggregateValue option for column listed above`); } columnValue = aggregateValueFn(columnValue, row, column); } return columnValue; }); values[column.id] = aggregateFn(leafValues, groupedValues); } else if (column.aggregate) { console.info({ column }); throw new Error(`React Table: Invalid column.aggregate option for column listed above`); } else { values[column.id] = null; } }); return values; }; const groupedFlatRows = []; const groupedRowsById = {}; const onlyGroupedFlatRows = []; const onlyGroupedRowsById = {}; const nonGroupedFlatRows = []; const nonGroupedRowsById = {}; // Recursively group the data const groupUpRecursively = (rows, depth = 0, parentId) => { // This is the last level, just return the rows if (depth === existingGroupBy.length) { return rows.map(row => ({ ...row, depth })); } const columnId = existingGroupBy[depth]; // Group the rows together for this level const rowGroupsMap = groupByFn(rows, columnId); // Peform aggregations for each group const aggregatedGroupedRows = Object.entries(rowGroupsMap).map(([groupByVal, groupedRows], index) => { let id = `${columnId}:${groupByVal}`; id = parentId ? `${parentId}>${id}` : id; // First, Recurse to group sub rows before aggregation const subRows = groupUpRecursively(groupedRows, depth + 1, id); // Flatten the leaf rows of the rows in this group const leafRows = depth ? flattenBy(groupedRows, 'leafRows') : groupedRows; const values = aggregateRowsToValues(leafRows, groupedRows, depth); const row = { id, isGrouped: true, groupByID: columnId, groupByVal, values, subRows, leafRows, depth, index }; subRows.forEach(subRow => { groupedFlatRows.push(subRow); groupedRowsById[subRow.id] = subRow; if (subRow.isGrouped) { onlyGroupedFlatRows.push(subRow); onlyGroupedRowsById[subRow.id] = subRow; } else { nonGroupedFlatRows.push(subRow); nonGroupedRowsById[subRow.id] = subRow; } }); return row; }); return aggregatedGroupedRows; }; const groupedRows = groupUpRecursively(rows); groupedRows.forEach(subRow => { groupedFlatRows.push(subRow); groupedRowsById[subRow.id] = subRow; if (subRow.isGrouped) { onlyGroupedFlatRows.push(subRow); onlyGroupedRowsById[subRow.id] = subRow; } else { nonGroupedFlatRows.push(subRow); nonGroupedRowsById[subRow.id] = subRow; } }); // Assign the new data return [groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById]; }, [manualGroupBy, groupBy, rows, flatRows, rowsById, allColumns, userAggregations, groupByFn]); const getAutoResetGroupBy = useGetLatest(autoResetGroupBy); useMountedLayoutEffect(() => { if (getAutoResetGroupBy()) { dispatch({ type: actions.resetGroupBy }); } }, [dispatch, manualGroupBy ? null : data]); Object.assign(instance, { preGroupedRows: rows, preGroupedFlatRow: flatRows, preGroupedRowsById: rowsById, groupedRows, groupedFlatRows, groupedRowsById, onlyGroupedFlatRows, onlyGroupedRowsById, nonGroupedFlatRows, nonGroupedRowsById, rows: groupedRows, flatRows: groupedFlatRows, rowsById: groupedRowsById, toggleGroupBy, setGroupBy }); } function prepareRow(row) { row.allCells.forEach(cell => { // Grouped cells are in the groupBy and the pivot cell for the row cell.isGrouped = cell.column.isGrouped && cell.column.id === row.groupByID; // Placeholder cells are any columns in the groupBy that are not grouped cell.isPlaceholder = !cell.isGrouped && cell.column.isGrouped; // Aggregated cells are not grouped, not repeated, but still have subRows cell.isAggregated = !cell.isGrouped && !cell.isPlaceholder && row.subRows?.length; }); } export function defaultGroupByFn(rows, columnId) { return rows.reduce((prev, row) => { const resKey = `${row.values[columnId]}`; prev[resKey] = Array.isArray(prev[resKey]) ? prev[resKey] : []; prev[resKey].push(row); return prev; }, {}); }