import { produce } from 'immer' import { isGridServerData } from '../../type-predicates' import type { ApplyPropsAction, UpdateExpandedRowsAction } from '../actions' import type { GridRowId, GridRowMeta } from '../../types' import { precalculateCollections } from '../../data-tools' import type { Collection } from '../../data-tools/types' import { createSelector } from '../utils' import { createReducer } from '../utils/createReducer' export type GridRowState = { collection: Collection expanded: Set expandedControlled: boolean treeCache: null | { rowCount: number levels: Map } _lastUpdate?: unknown } const initialState: GridRowState = { collection: { ids: [], entities: new Map(), meta: new Map(), }, expanded: new Set(), expandedControlled: false, treeCache: null, } export const selectRow = (state: GridRowState, rowId: GridRowId) => state.collection.entities.get(rowId) const NO_META: GridRowMeta = {} export const selectRowMeta = (state: GridRowState, rowId: GridRowId) => state.collection.meta.get(rowId) || NO_META export default createReducer({ initialState, reducers: { applyProps(state, action: ApplyPropsAction) { const updatedState = produce(state, (draft) => { draft.expandedControlled = !!action.payload.expandedRows if (action.payload.expandedRows !== undefined) { draft.expanded = action.payload.expandedRows } }) /* Load rows */ if (state._lastUpdate !== action.payload.rows) { return produce(updatedState, (draft) => { draft._lastUpdate = action.payload.rows if (isGridServerData(action.payload.rows)) { const ids = action.payload.rows.ids const entities = action.payload.rows.data instanceof Map ? action.payload.rows.data : new Map( Object.entries(action.payload.rows.data) ) const meta = action.payload.rows.meta instanceof Map ? action.payload.rows.meta : action.payload.rows.meta ? new Map( Object.entries(action.payload.rows.meta) ) : new Map() draft.collection.ids = ids draft.collection.entities = entities draft.collection.meta = meta draft.treeCache = precalculateCollections({ ids, entities, meta, }) if (!updatedState.expandedControlled) { const adjustedExpanded = new Set( updatedState.expanded ) let expandedChanged = false for (const [id, rowMeta] of meta) { const oldMeta = updatedState.collection.meta.get(id) if ( rowMeta.type === 'group' && oldMeta?.type !== 'group' ) { /* This group is new, we expand it by default */ adjustedExpanded.add(id) expandedChanged = true } } if (expandedChanged) { draft.expanded = adjustedExpanded } } } else { const newIds: string[] = [] const newEntities = new Map() action.payload.rows.forEach((row) => { newEntities.set(row.id, row) newIds.push(row.id) }) draft.collection.ids = newIds draft.collection.entities = newEntities draft.collection.meta = new Map() draft.treeCache = null } }) } return updatedState }, updateExpandedRows(state, action: UpdateExpandedRowsAction) { return { ...state, expanded: action.payload, } }, }, }) export const selectRowCollection = (state: GridRowState): Collection => state.collection export const selectRowLevel = ( state: GridRowState, rowId: GridRowId ): number => (state.treeCache ? (state.treeCache.levels.get(rowId) ?? -1) : -1) export const selectRowLevelMap = (state: GridRowState) => state.treeCache?.levels export const selectIsRowExpandable = ( state: GridRowState, rowId: GridRowId ): boolean => { const meta = selectRowMeta(state, rowId) return meta?.type === 'group' || meta?.type === 'tree' } export const selectIsRowLoaded = (state: GridRowState, rowId: GridRowId) => !!selectRow(state, rowId) export const selectAreExpandedRowsControlled = (state: GridRowState) => state.expandedControlled export const selectExpandedRows = (state: GridRowState) => state.expanded export const selectIsRowExpanded = (state: GridRowState, rowId: GridRowId) => state.expanded.has(rowId) const _selectRowMeta = (state: GridRowState) => state.collection.meta export const selectRowsExpanded = (state: GridRowState) => state.expanded export const selectCanDragRow = (state: GridRowState, rowId: GridRowId) => { const rowMeta = state.collection.meta.get(rowId) if (!rowMeta) { return true } if (rowMeta.type === 'group') { return rowMeta.preventDrag == null ? false : !rowMeta.preventDrag } return !rowMeta.preventDrag } export const generateSelectors = () => { const selectAllExpandable = createSelector([_selectRowMeta], (rowMeta) => { const expandable = new Set() for (const [id, meta] of rowMeta) { if (meta.type === 'group' || meta.type === 'tree') { expandable.add(id) } } return expandable }) const selectExpandedState = createSelector( [selectRowsExpanded, selectAllExpandable], (expanded, allExpandable) => { if (expanded.size === 0) { return 'none' } const filteredExpanded = new Set( [...expanded].filter((id) => allExpandable.has(id)) ) if (filteredExpanded.size !== allExpandable.size) { return 'some' } return 'all' } ) const selectAllDescendantsForIds = createSelector( [_selectRowMeta, (_, rowIds: GridRowId[]) => rowIds], (rowCollectionMeta, rowIds) => { const ids = new Set() const collect = ( id: GridRowId, memo: Set, collectId = false ) => { if (collectId) { memo.add(id) } const rowMeta = rowCollectionMeta.get(id) if (rowMeta?.children?.length) { rowMeta.children.forEach((childId) => collect(childId, memo, true) ) } } rowIds.forEach((rowId) => collect(rowId, ids)) return ids } ) const selectAllExpandableDescendants = createSelector( [_selectRowMeta, (_, rowId: GridRowId) => rowId], (rowCollectionMeta, rowId) => { const expandable = new Set() const collect = (id: GridRowId, memo: Set) => { const rowMeta = rowCollectionMeta.get(id) if (rowMeta?.type === 'group' || rowMeta?.type === 'tree') { memo.add(id) if (rowMeta?.children?.length) { rowMeta.children.forEach((childId) => collect(childId, memo) ) } } } collect(rowId, expandable) return expandable } ) return { selectAllExpandable, selectExpandedState, selectAllDescendantsForIds, selectAllExpandableDescendants, } }