@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
318 lines (306 loc) • 10.6 kB
JavaScript
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;
}, {});
}