import * as React from 'react' ; import { ScrollablePane , ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane' ; import { Sticky , StickyPositionType } from 'office-ui-fabric-react/lib/Sticky' ; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities' ; import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling' ; import { DetailsList , DetailsListLayoutMode , Selection , SelectionMode , IGroup , IDetailsHeaderProps } from 'office-ui-fabric-react/lib/DetailsList' ; import { IListViewProps , IListViewState , IViewField , IGrouping , GroupOrder } from './IListView' ; import { ConstrainMode , IColumn , IDetailsList , IGroupRenderProps } from 'office-ui-fabric-react/lib/components/DetailsList' ; import { findIndex , has , sortBy , isEqual , cloneDeep } from '@microsoft/sp-lodash-subset' ; import { FileTypeIcon , IconType } from '../fileTypeIcon/index' ; import * as strings from 'ControlStrings' ; import { IGroupsItems } from './IListView' ; import * as telemetry from '../../common/telemetry' ; import { DragDropFiles } from "../dragDropFiles" ; import filter from 'lodash/filter' ; import omit from 'lodash/omit' ; import functions from 'lodash/functions' ; import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox' ; import { Guid } from '@microsoft/sp-core-library' ; const classNames = mergeStyleSets ( { wrapper : { height : '50vh' , position : 'relative' } } ) ; /**
* Wrap the listview in a scrollable pane if sticky header = true
*/ const ListViewWrapper = ( { stickyHeader , children , className } ) => ( stickyHeader ? < div className = { `${ classNames . wrapper } ${ className ?? "" }` } > < ScrollablePane scrollbarVisibility = { ScrollbarVisibility . auto } > { children } < /ScrollablePane> < /div> : children ) ; /**
* Lock the searchbox when scrolling if sticky header = true
*/ const SearchBoxWrapper = ( { stickyHeader , children } ) => ( stickyHeader ? < Sticky stickyPosition = { StickyPositionType . Header } > { children } < /Sticky> : children ) ; /**
 * File type icon component
 */ export class ListView extends React . Component < IListViewProps , IListViewState > { private _selection : Selection ; private originalItems : any [ ] ; private originalGroups : IGroup [ ] ; private originalColumns : IColumn [ ] ; constructor ( props ) { super ( props ) ; telemetry . track ( 'ReactListView' , { viewFields : ! ! props . viewFields , groupByFields : ! ! props . groupByFields , selectionMode : ! ! props . selectionMode , selection : ! ! props . selection , defaultSelection : ! ! props . defaultSelection } ) ; this . state = { filterValue : this . props . defaultFilter } ; if ( this . props . selection ) { this . _selection = new Selection ( { onSelectionChanged : ( ) => this . props . selection ( this . _selection . getSelection ( ) ) } ) ; } } /**
     * Lifecycle hook when component is mounted
     */ public componentDidMount ( ) { this . _processProperties ( this . props ) ; } param prevProps param prevState public componentWillReceiveProps ( nextProps ) { const modifiedNextProps = this . _filterFunctions ( nextProps ) ; const modifiedProps = this . _filterFunctions ( this . props ) ; if ( ! isEqual ( modifiedProps , modifiedNextProps ) ) { if ( this . _selection ) { if ( ! isEqual ( modifiedNextProps . items , modifiedProps . items ) ) { this . _selection . setItems ( nextProps . items , true ) ; } if ( ! isEqual ( modifiedNextProps . defaultSelection , modifiedProps . defaultSelection ) ) { this . _selection . setAllSelected ( false ) ; this . _setSelectedItems ( nextProps ) ; } } this . _processProperties ( nextProps ) ; } } /**
     * Select all the items that should be selected by default
     */ private _setSelectedItems ( props ) { if ( props . items && props . items . length > 0 && props . defaultSelection && props . defaultSelection . length > 0 ) { for ( const index of props . defaultSelection ) { if ( index > - 1 ) { this . _selection . setIndexSelected ( index , true , false ) ; } } } } param items param groupByFields private _getGroups ( items , groupByFields , level = 0 , startIndex = 0 ) { let groups = [ ] ; let updatedItemsOrder = [ ] ; if ( groupByFields ) { const groupField = groupByFields [ level ] ; if ( groupByFields && groupByFields . length > 0 ) { const groupedItems = { } ; items . forEach ( ( item ) => { let groupName = item [ groupField . name ] ; if ( typeof groupName === "undefined" ) { groupName = strings . ListViewGroupEmptyLabel ; } if ( typeof groupName === "number" ) { groupName = `${ groupName }.` ; } if ( typeof groupedItems [ groupName ] === "undefined" ) { groupedItems [ groupName ] = [ ] ; } groupedItems [ groupName ] . push ( item ) ; } ) ; const sortedGroups = { } ; let groupNames = Object . keys ( groupedItems ) ; groupNames = groupField . order === GroupOrder . ascending ? groupNames . sort ( ) : groupNames . sort ( ) . reverse ( ) ; groupNames . forEach ( ( key ) => { sortedGroups [ key ] = groupedItems [ key ] ; } ) ; for ( const groupItems in sortedGroups ) { const totalItems = groupedItems [ groupItems ] . length ; const group = { name : groupItems === "undefined" ? strings . ListViewGroupEmptyLabel : groupItems , key : groupItems === "undefined" ? strings . ListViewGroupEmptyLabel : groupItems , startIndex : startIndex , count : totalItems , } ; if ( groupByFields [ level + 1 ] ) { const subGroup = this . _getGroups ( groupedItems [ groupItems ] , groupByFields , ( level + 1 ) , startIndex ) ; subGroup . items . forEach ( ( item ) => { updatedItemsOrder . push ( item ) ; } ) ; group . children = subGroup . groups ; } else { groupedItems [ groupItems ] . forEach ( ( item ) => { updatedItemsOrder . push ( item ) ; } ) ; } startIndex = startIndex + totalItems ; groups . push ( group ) ; } } } return { items : updatedItemsOrder , groups } ; } /**
     * Process all the component properties
     */ private _processProperties ( props ) { const { dragDropFiles , items , iconFieldName , viewFields , groupByFields , showFilter } = props ; let tempState = cloneDeep ( this . state ) ; let columns = null ; if ( typeof items !== 'undefined' && items !== null ) { tempState . items = this . _flattenItems ( items ) ; } if ( iconFieldName ) { if ( columns === null ) { columns = [ ] ; } const iconColumn = this . _createIconColumn ( iconFieldName ) ; columns . push ( iconColumn ) ; } if ( viewFields ) { if ( columns === null ) { columns = [ ] ; } columns = this . _createColumns ( viewFields , columns ) ; } tempState . columns = columns ; if ( tempState . items ) { const grouping = this . _getGroups ( tempState . items , groupByFields ) ; if ( grouping . groups . length > 0 ) { tempState . groups = grouping . groups ; tempState . items = grouping . items ; } else { tempState . groups = null ; } } this . originalItems = tempState . items ; this . originalGroups = tempState . groups ; this . originalColumns = tempState . columns ; const { filterValue } = this . state ; if ( filterValue && showFilter ) { this . setState ( { columns : tempState . columns } ) ; this . _updateFilterValue ( filterValue ) ; } else { this . setState ( tempState ) ; } } param items private _flattenItems ( items ) { const flattenItems = items . map ( item => { return this . _flattenItem ( item ) ; } ) ; return flattenItems ; } param item private _flattenItem ( item ) { let flatItem = { } ; for ( let parentPropName in item ) { if ( ! item . hasOwnProperty ( parentPropName ) ) continue ; if ( ( typeof item [ parentPropName ] ) === 'object' ) { const flatObject = this . _flattenItem ( item [ parentPropName ] ) ; for ( let childPropName in flatObject ) { if ( ! flatObject . hasOwnProperty ( childPropName ) ) continue ; flatItem [ `${ parentPropName }.${ childPropName }` ] = flatObject [ childPropName ] ; } } else { flatItem [ parentPropName ] = item [ parentPropName ] ; } } if ( ! flatItem . key ) { flatItem . key = flatItem . ID || flatItem . Id ; if ( ! flatItem . key ) { flatItem . key = Guid . newGuid ( ) . toString ( ) ; } } return flatItem ; } param iconField private _createIconColumn ( iconFieldName ) { return { key : 'fileType' , name : 'File Type' , iconName : 'Page' , isIconOnly : true , fieldName : 'fileType' , minWidth : 16 , maxWidth : 16 , onRender : ( item ) => { return ( < FileTypeIcon type = { IconType . image } path = { item [ iconFieldName ] } / > ) ; } } ; } param viewFields private _createColumns ( viewFields , crntColumns ) { viewFields . forEach ( field => { crntColumns . push ( { key : field . name , name : field . displayName || field . name , fieldName : field . name , minWidth : field . minWidth || 50 , maxWidth : field . maxWidth , isResizable : field . isResizable , onRender : this . _fieldRender ( field ) , onColumnClick : this . _columnClick } ) ; } ) ; return crntColumns ; } param field private _fieldRender ( field ) { if ( field . render ) { return field . render ; } if ( field . linkPropertyName ) { return ( item , index ? , column ? ) => { return < a href = { item [ field . linkPropertyName ] } > { item [ column . fieldName ] } < /a>; } ; } } param ev param column private _columnClick = ( ev , column ) : void => { const columnIdx = findIndex ( this . props . viewFields , field => field . name === column . key ) ; if ( columnIdx !== - 1 ) { const field = this . props . viewFields [ columnIdx ] ; if ( has ( field , 'sorting' ) ) { if ( field . sorting ) { const sortDescending = typeof column . isSortedDescending === 'undefined' ? false : ! column . isSortedDescending ; const sortedItems = this . _sortItems ( this . state . items , column . key , sortDescending ) ; const sortedColumns = this . state . columns . map ( c => { if ( c . key === column . key ) { c . isSortedDescending = sortDescending ; c . isSorted = true ; } else { c . isSorted = false ; c . isSortedDescending = false ; } return c ; } ) ; const groupedItems = this . _getGroups ( sortedItems , this . props . groupByFields ) ; this . setState ( { items : groupedItems . groups . length > 0 ? groupedItems . items : sortedItems , columns : sortedColumns , groups : groupedItems . groups . length > 0 ? groupedItems . groups : null , } ) ; } } } } ; param newValue private _updateFilterValue = ( filterValue ) => { let items = cloneDeep ( this . originalItems ) ; let groups = cloneDeep ( this . originalGroups ) ; const columns = cloneDeep ( this . originalColumns ) ; if ( filterValue && items && items . length > 0 ) { items = this . _executeFiltering ( filterValue , items , columns ) ; const grouping = this . _getGroups ( items , this . props . groupByFields ) ; if ( grouping . groups . length > 0 ) { groups = grouping . groups ; items = grouping . items ; } else { groups = null ; } } this . setState ( { filterValue , items , groups } ) ; } ; param items param columnName param descending private _sortItems ( items , columnName , descending = false ) { if ( this . props . sortItems ) { return this . props . sortItems ( items , columnName , descending ) ; } const ascItems = sortBy ( items , [ columnName ] ) ; const sortedItems = descending ? ascItems . reverse ( ) : ascItems ; return sortedItems ; } param filterValue param items param columns private _executeFiltering ( filterValue , items , columns ) { const filterSeparator = ":" ; let filterColumns = [ ... columns ] ; if ( filterValue && filterValue . indexOf ( filterSeparator ) >= 0 ) { const columnName = filterValue . split ( filterSeparator ) [ 0 ] ; filterValue = filterValue . split ( filterSeparator ) [ 1 ] ; filterColumns = filter ( columns , column => column . fieldName === columnName || column . name === columnName ) ; } return this . _getFilteredItems ( filterValue , items , filterColumns ) ; } param filterValue param items param columns private _getFilteredItems ( filterValue , items , columns ) { if ( ! filterValue ) { return items ; } let result = [ ] ; for ( const item of items ) { let addItemToResultSet = false ; for ( const viewField of columns ) { if ( this . _doesPropertyContainsValue ( item , viewField . fieldName , filterValue ) ) { addItemToResultSet = true ; break ; } if ( this . _doesPropertyContainsValue ( item , viewField . name , filterValue ) ) { addItemToResultSet = true ; break ; } } if ( addItemToResultSet ) { result . push ( item ) ; } } return result ; } param item param property param filterValue private _doesPropertyContainsValue ( item , property , filterValue ) { const propertyValue = item [ property ] ; let result = false ; if ( propertyValue ) { result = propertyValue . toString ( ) . toLowerCase ( ) . indexOf ( filterValue . toLowerCase ( ) ) >= 0 ; } return result ; } private _filterFunctions ( p ) { const modifiedProps = omit ( p , functions ( p ) ) ; if ( modifiedProps . items ) { modifiedProps . items = modifiedProps . items . map ( i => omit ( i , functions ( i ) ) ) ; } if ( modifiedProps . viewFields ) { modifiedProps . viewFields = modifiedProps . viewFields . map ( vf => omit ( vf , functions ( vf ) ) as IViewField ) ; } return modifiedProps ; } param props param defaultRender private _onRenderDetailsHeader : IRenderFunction < IDetailsHeaderProps > = ( props , defaultRender ) => { if ( ! props ) { return null ; } if ( this . props . stickyHeader ) { return ( < Sticky stickyPosition = { StickyPositionType . Header } isScrollSynced > { defaultRender ! ( { ... props , } ) } < /Sticky>); } return defaultRender ! ( props ) ; } ; /**
     * Default React component render method
     */ public render ( ) { let groupProps = { } ; let { showFilter , filterPlaceHolder , dragDropFiles , stickyHeader , selectionMode , compact , className , listClassName } = this . props ; let { filterValue , items , columns , groups } = this . state ; if ( this . props . selectionMode === SelectionMode . single ) { groupProps = { headerProps : { onToggleSelectGroup : ( ) => null , onGroupHeaderClick : ( ) => null , } } ; } return ( < ListViewWrapper stickyHeader = { ! ! stickyHeader } className = { className } > < DragDropFiles enable = { dragDropFiles } iconName = "BulkUpload" labelMessage = { strings . UploadFileHeader } onDrop = { dragDropFiles ? ( files ) => { this . props . onDrop ( files ) ; } : [ ] } > { showFilter && < SearchBoxWrapper stickyHeader = { ! ! stickyHeader } > < SearchBox placeholder = { filterPlaceHolder || strings . ListViewFilterLabel } onSearch = { this . _updateFilterValue } onChange = { ( e , value ) => this . _updateFilterValue ( value ) } value = { filterValue } / > < /SearchBoxWrapper>} { ! ! items && < DetailsList key = "ListViewControl" items = { items } columns = { columns } groups = { groups } selectionMode = { selectionMode || SelectionMode . none } selectionPreservedOnEmptyClick = { true } selection = { this . _selection } layoutMode = { DetailsListLayoutMode . justified } compact = { compact } setKey = 'ListViewControl' groupProps = { groupProps } className = { listClassName } onRenderDetailsHeader = { this . _onRenderDetailsHeader } componentRef = { ref => { if ( ref ) { ref . forceUpdate ( ) ; } } } />} < /DragDropFiles> < /ListViewWrapper>); } }