/** Helpers for managing a DetailsList. */ const R = require("ramda") import { IColumn, ColumnActionsMode } from 'office-ui-fabric-react/lib/DetailsList' export const OTHER: string = "OTHER" export type SortOrder = "asc" | "desc" | "OTHER" export type SortOrderState = SortOrder | "FIRST" /** Sorting state for a single column. */ export interface ColumnSortInfo { /** Sorting direction. */ direction: SortOrder /** Position in column sorting order e.g. if firstname to be sorted *after* lastname, * pos=1 for firstname and pos=0 for lastname. */ position: number } /** Group of individual column sort infos. State key is column index or IColumn.key. */ export type SortingState = { [col: number]: ColumnSortInfo [col: string]: ColumnSortInfo } /** Transition table for sorting direction state machine. */ export interface SortOrderTransitions extends Partial> { /** Starting state is required. */ FIRST: SortOrder } /** * Default sort order state machine table. You can also store these as state in * in your component and provide them to the byColumns* functions. */ export const defaultOrder: SortOrderTransitions = { FIRST: "asc", OTHER: "asc", asc: "desc", desc: "OTHER" } /** * Previous SortingState => new SortingState (state machine). Only allows single * column sorting. Cycles through sorting order if its already sorted on that column. */ export function byColumn({ current, transitions = defaultOrder, selectedColumn = -1, defaultPosition = 0}:{ current: SortingState transitions: SortOrderTransitions selectedColumn: number|string defaultPosition?: number }): SortingState { if(typeof selectedColumn === "number" && selectedColumn < 0) return current let nextState: SortOrder | undefined = transitions.FIRST // find if current column is in current state, so we can cycle it const maybeCurCol = current[selectedColumn] if(maybeCurCol) { nextState = transitions[maybeCurCol.direction] // returns next state if(!nextState) return {} } return { [selectedColumn]: { direction: nextState, position: defaultPosition } } } /** * Updates a multi column sorting state by updating the selectedColumn. * If selectedColumn is new to the sorting state, it is placed last in the * positions. */ export function byColumns({ current, transitions = defaultOrder, selectedColumn = -1}:{ current: SortingState, transitions: SortOrderTransitions, selectedColumn: number|string }): SortingState { // get last position, assume sorting state is "short" const max = Math.max(...Object.keys(current ? current :{}).map(k => current[k].position)) + 1 const alreadyExists = current ? (current[selectedColumn] ? true : false): false let newOrUsed = alreadyExists ? byColumn({current, transitions, selectedColumn, defaultPosition: current[selectedColumn].position}): byColumn({current, transitions, selectedColumn, defaultPosition: max}) return Object.assign({}, current, newOrUsed) } /** Sort data. */ export type Sorter = (a: Array) => Array /** Create Sorter functions given sorting information. */ export type SortFunctionFactory = (info: Array<{property: string, direction: SortOrder}>) => Sorter /** Get the property name of the data accessor used for sorting from an IColumn. */ function getSortAttribute(c: IColumn): string { if(c.data && c.data.sortAttribute) return c.data.sortAttribute else return c.fieldName } /** * Create a Sorter given columns, sorting state and a sort function factory. * Uses IColumn.key to lookup fieldname|data.sortAttribute if sorting state has a property name as a key, * or IColumn[index] to lookup the column. */ export const sorter = ({ columns, state, factory = ramdaSortFunctionFactory}: { columns: Array, factory?: SortFunctionFactory, state: SortingState }): Sorter => { // flatten sorting state const x = Object.keys(state).map(k => { const sortInfo = state[k] let p: string = (typeof k === "number") ? getSortAttribute(columns[k]): getSortAttribute(columns.find(c => c.key === k)!) return { property: p, direction: sortInfo.direction, order: sortInfo.position } }) return factory ? factory(R.sort(i => i.order, x)) : ramdaSortFunctionFactory(R.sort(i => i.order, x)) } /** Sort function factory that uses ramda sortWith. */ export function ramdaSortFunctionFactory(info: Array<{property: string, direction: SortOrder}>): Sorter { const comparators = info.filter(i => i.direction !== OTHER). map(i => { if(i.direction === "asc") return R.ascend(R.prop(i.property)) else return R.descend(R.prop(i.property)) }) return (comparators.length === 0 ? R.identity: R.sortWith(comparators)) as Sorter } /** Update column definitios based on the column key and the map of updates. */ export function updateColumns(columns: IColumn[], updates: { [key:string]: Partial}): IColumn[] { return columns.map(c => { const update = { ...c , ...updates[c.key]} return update }) } /** * Enhance a list of column-like information based on the sortState. All columns are * are sortable unless data.isSortable = false. * You can set a DetailsList.onColumnHeaderClick instead of passing in onSortColumn. */ export function augmentColumns(cols: any[], sortState: SortingState, onSortColumn?: (c: IColumn) => void, getMoreProps?: (c: IColumn, idx: number) => Record): IColumn[] { return cols.map((c, idx) => { if(c.data && typeof c.data.isSortable === "boolean" && !c.data.isSortable) return { ...c } const sortInfo: ColumnSortInfo | undefined = sortState[c.key] const isSorted = sortInfo ? (sortInfo.direction !== OTHER) : undefined const isSortedDescending = isSorted ? (sortInfo.direction === "desc" ? true : false) : undefined const moreProps = getMoreProps ? getMoreProps(c, idx) : {} const onCC = onSortColumn ? {onColumnClick: (x, col) => { if (col) onSortColumn(col) }}: {} return { ...c, ...moreProps, isSorted, isSortedDescending, ...onCC, } }) } /** * Given a request to sort, update critical key parts of the sorting infrastructure. * You will need to sort your data with the returned sorter function. The returned * columns are updated with the new sort state (e.g. isSorted, isSortedDescending). */ export function onSortColumn(c: IColumn, columns: IColumn[], sortState: SortingState, transitions: SortOrderTransitions, factory: SortFunctionFactory = ramdaSortFunctionFactory) { // create the new sort state const state = byColumns({ current: sortState, transitions, selectedColumn: c.key, }) // augment the columns with the new sort state const newColumns = augmentColumns(columns, state) // create the new sorter function const newSorter = sorter({ columns: newColumns, state, factory, }) return { columns: newColumns, sortState: state, sorter: newSorter, } }