import * as React from 'react' ; import { ISearchFiltersContainerProps } from './ISearchFiltersContainerProps' ; import { TemplateRenderer } from '../../../controls/TemplateRenderer/TemplateRenderer' ; import { ISearchFiltersContainerState } from './ISearchFiltersContainerState' ; import { isEqual , cloneDeep , sortBy } from '@microsoft/sp-lodash-subset' ; import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle" ; import * as webPartStrings from 'SearchFiltersWebPartStrings' ; import * as commonStrings from 'CommonStrings' ; import update from 'immutability-helper' ; import { MessageBar , MessageBarType } from 'office-ui-fabric-react' ; import { IDataFilterInternal , IDataFilterValueInternal , IDataFilterConfiguration , IDataFilterResult , IDataFilterResultValue , IDataFilter , FilterComparisonOperator , IDataFilterInfo , ExtensibilityConstants , FilterConditionOperator } from '@pnp/modern-search-extensibility' ; import { ISearchFiltersTemplateContext } from '../../../models/common/ITemplateContext' ; import { flatten } from '@microsoft/sp-lodash-subset' ; import { DisplayMode , Log } from '@microsoft/sp-core-library' ; import { DataFilterHelper } from '../../../helpers/DataFilterHelper' ; import { UrlHelper } from '../../../helpers/UrlHelper' ; import { BuiltinFilterTemplates } from '../../../layouts/AvailableTemplates' ; const DEEPLINK_QUERYSTRING_PARAM = 'f' ; export default class SearchFiltersContainer extends React . Component < ISearchFiltersContainerProps , ISearchFiltersContainerState > { private componentRef : React . RefObject < any > ; public constructor ( props ) { super ( props ) ; this . state = { currentUiFilters : [ ] , submittedFilters : [ ] } ; this . componentRef = React . createRef ( ) ; this . onFilterValuesUpdated = this . onFilterValuesUpdated . bind ( this ) ; } public render ( ) { let renderWpContent = null ; let templateContent = null ; let renderTitle = null ; renderTitle = < WebPartTitle displayMode = { this . props . webPartTitleProps . displayMode } title = { this . props . webPartTitleProps . title } updateProperty = { this . props . webPartTitleProps . updateProperty } className = { this . props . webPartTitleProps . className } / > ; if ( this . state . currentUiFilters . length === 0 ) { if ( this . props . webPartTitleProps . displayMode === DisplayMode . Edit ) { renderWpContent = < MessageBar messageBarType = { MessageBarType . info } > { webPartStrings . General . NoAvailableFilterMessage } < /MessageBar>; } } else { templateContent = this . props . templateService . getTemplateMarkup ( this . props . templateContent ) ; const templateContext = this . getTemplateContext ( ) ; renderWpContent = < TemplateRenderer key = { JSON . stringify ( templateContext ) } templateContent = { templateContent } templateContext = { templateContext } templateService = { this . props . templateService } instanceId = { this . props . instanceId } / > ; } return < div ref = { this . componentRef } data - instance - id = { this . props . instanceId } > { renderTitle } { renderWpContent } < /div>; } public componentDidMount ( ) { this . bindFilterEvents ( ) ; this . bindApplyFiltersEvents ( ) ; this . bindClearFiltersEvents ( ) ; this . bindFilterValueOperatorUpdated ( ) ; this . getFiltersToDisplay ( this . props . availableFilters , [ ] , this . props . filtersConfiguration ) ; this . getFiltersDeepLink ( ) ; this . _handleQueryStringChange ( ) ; } public componentDidUpdate ( prevProps , prevState ) { if ( ! isEqual ( prevProps . selectedLayoutKey , this . props . selectedLayoutKey ) || ! isEqual ( prevProps . properties , this . props . properties ) ) { const updatedfilters = this . resetSelectedFilterValues ( this . state . currentUiFilters ) ; const submittedFilters = this . getSelectedFiltersFromUIFilters ( updatedfilters ) ; this . getFiltersToDisplay ( this . props . availableFilters , [ ] , this . props . filtersConfiguration ) ; this . resetFiltersDeepLink ( ) ; this . props . onUpdateFilters ( submittedFilters ) ; } if ( ! isEqual ( prevProps . availableFilters , this . props . availableFilters ) ) { this . getFiltersToDisplay ( this . props . availableFilters , this . state . currentUiFilters , this . props . filtersConfiguration ) ; const submittedFilters = this . getSelectedFiltersFromUIFilters ( this . state . currentUiFilters ) ; this . setState ( { submittedFilters : submittedFilters } ) ; } } param availableFilters param currentUIFilters param filtersConfiguration private getFiltersToDisplay ( availableFilters , currentUiFilters , filtersConfiguration ) { const updatedFilters = availableFilters . map ( availableFilter => { let values = [ ] ; const filterConfiguration = DataFilterHelper . getConfigurationForFilter ( availableFilter , filtersConfiguration ) ; if ( filterConfiguration ) { const selectedFilterIdx = currentUiFilters . map ( selectedFilter => { return selectedFilter . filterName ; } ) . indexOf ( availableFilter . filterName ) ; if ( availableFilter . values . length === 0 ) { if ( selectedFilterIdx !== - 1 ) { values = currentUiFilters [ selectedFilterIdx ] . values . map ( value => { if ( ( ( value . selected || value . selectedOnce ) && filterConfiguration . isMulti ) || ( value . selected && ! filterConfiguration . isMulti ) ) { value . count = 0 ; } else { return null ; } return value ; } ) . filter ( value => value ) ; } } else { values = availableFilter . values . map ( ( availableValue ) => { const filterValueInternal = { name : availableValue . name , selected : false , selectedOnce : false , disabled : false , value : availableValue . value , count : availableValue . count } ; if ( selectedFilterIdx !== - 1 ) { const valueIdx = currentUiFilters [ selectedFilterIdx ] . values . map ( value => { return value . value ; } ) . indexOf ( availableValue . value ) ; if ( valueIdx === - 1 ) { return filterValueInternal ; } else { const updatedValue = currentUiFilters [ selectedFilterIdx ] . values [ valueIdx ] ; updatedValue . count = availableValue . count ; updatedValue . name = availableValue . name ; return updatedValue ; } } else { return filterValueInternal ; } } ) ; } if ( selectedFilterIdx !== - 1 ) { const additionalValues = currentUiFilters [ selectedFilterIdx ] . values . map ( value => { const valueIdx = values . map ( v => { return v . value ; } ) . indexOf ( value . value ) ; if ( valueIdx === - 1 && value . selected ) { return value ; } } ) ; values = values . concat ( additionalValues . filter ( value => value ) ) ; } const selectedOnce = selectedFilterIdx !== - 1 && currentUiFilters [ selectedFilterIdx ] . selectedOnce ? currentUiFilters [ selectedFilterIdx ] . selectedOnce : values . filter ( value => { return value . selectedOnce ; } ) . length > 0 ; const hasSelectedValues = values . filter ( value => { return value . selected ; } ) . length > 0 ; const currentSelectedValuesInUiForFilter = values . filter ( value => { return value . selected ; } ) . map ( v => v . value ) . sort ( ) ; const alreadySubmittedValuesForFilter = flatten ( this . state . submittedFilters . filter ( s => s . filterName === availableFilter . filterName ) . map ( v => v . values ) ) . map ( t => t . value ) . sort ( ) ; const canApply = ! isEqual ( currentSelectedValuesInUiForFilter , alreadySubmittedValuesForFilter ) ; const canClear = alreadySubmittedValuesForFilter . length > 0 ; values = values . map ( value => { if ( ! filterConfiguration . isMulti && hasSelectedValues && ! value . selected ) { value . disabled = true ; } else { value . disabled = false ; } return value ; } ) ; const filterOperator = currentUiFilters [ selectedFilterIdx ] ? currentUiFilters [ selectedFilterIdx ] . operator : filterConfiguration . operator ; const filterResultInternal = { displayName : filterConfiguration . displayValue ? filterConfiguration . displayValue : availableFilter . filterName , filterName : availableFilter . filterName , isMulti : ! filterConfiguration . isMulti ? false : filterConfiguration . isMulti , showCount : ! filterConfiguration . showCount ? false : filterConfiguration . showCount , expandByDefault : ! filterConfiguration . expandByDefault ? false : filterConfiguration . expandByDefault , selectedOnce : selectedOnce , selectedTemplate : filterConfiguration . selectedTemplate , hasSelectedValues : hasSelectedValues , values : values , operator : filterOperator , sortIdx : filterConfiguration . sortIdx , canApply : canApply , canClear : canClear } ; return filterResultInternal ; } } ) ; this . setState ( { currentUiFilters : update ( this . state . currentUiFilters , { $set : sortBy ( updatedFilters . filter ( updatedFilter => updatedFilter ) , 'sortIdx' ) } ) } ) ; } param filterInfo private onFilterValuesUpdated ( filterInfo ) { let currentUiFilters = [ ] ; const filterConfigIdx = this . props . filtersConfiguration . map ( filter => { return filter . filterName ; } ) . indexOf ( filterInfo . filterName ) ; if ( filterConfigIdx !== - 1 ) { const filterConfiguration = this . props . filtersConfiguration [ filterConfigIdx ] ; const filterIdx = this . state . currentUiFilters . map ( filter => { return filter . filterName ; } ) . indexOf ( filterInfo . filterName ) ; if ( filterIdx !== - 1 ) { currentUiFilters = cloneDeep ( this . state . currentUiFilters ) ; if ( filterInfo . operator ) { currentUiFilters = update ( currentUiFilters , { [ filterIdx ] : { operator : { $set : filterInfo . operator } } } ) ; } filterInfo . filterValues . map ( filterValue => { const filterValueInternal = { selected : filterValue . selected , name : filterValue . name , value : filterValue . value , operator : filterValue . operator , selectedOnce : true } ; const valueIdx = currentUiFilters [ filterIdx ] . values . map ( value => { return value . value ; } ) . indexOf ( filterValue . value ) ; if ( valueIdx === - 1 ) { currentUiFilters = update ( currentUiFilters , { [ filterIdx ] : { values : { $push : [ filterValueInternal ] } } } ) ; } else { currentUiFilters = update ( currentUiFilters , { [ filterIdx ] : { values : { [ valueIdx ] : { $set : filterValueInternal } } } } ) ; } } ) ; } else { const filterValuesInternal = filterInfo . filterValues . map ( filterValue => { return { selected : filterValue . selected , name : filterValue . name , value : filterValue . value , selectedOnce : true } ; } ) ; const filterResultInternal = { displayName : filterConfiguration . displayValue ? filterConfiguration . displayValue : filterInfo . filterName , filterName : filterInfo . filterName , hasSelectedValues : filterInfo . filterValues . filter ( value => value . selected ) . length > 0 , selectedOnce : true , isMulti : ! filterConfiguration . isMulti ? false : filterConfiguration . isMulti , showCount : ! filterConfiguration . showCount ? false : filterConfiguration . showCount , expandByDefault : ! filterConfiguration . expandByDefault ? false : filterConfiguration . expandByDefault , values : filterValuesInternal , operator : filterInfo . operator ? filterInfo . operator : currentUiFilters [ filterIdx ] . operator , selectedTemplate : filterConfiguration . selectedTemplate , sortIdx : filterConfiguration . sortIdx } ; currentUiFilters = update ( this . state . currentUiFilters , { $push : [ filterResultInternal ] } ) ; } if ( ! filterConfiguration . isMulti || filterInfo . forceUpdate ) { const submittedFilters = this . getSelectedFiltersFromUIFilters ( currentUiFilters ) ; this . setState ( { submittedFilters : submittedFilters } , ( ) => { this . props . onUpdateFilters ( submittedFilters ) ; this . setFiltersDeepLink ( submittedFilters ) ; if ( filterConfiguration . isMulti ) { this . forceUpdate ( ) ; } } ) ; } this . getFiltersToDisplay ( this . props . availableFilters , currentUiFilters , this . props . filtersConfiguration ) ; } } param currentUiFilters private getSelectedFiltersFromUIFilters ( currentUiFilters ) { const selectedFilters = currentUiFilters . map ( selectedFilter => { const newSelectedFilter = cloneDeep ( selectedFilter ) ; if ( selectedFilter . values . some ( v => v . selected ) && ! selectedFilter . isMulti && selectedFilter . selectedTemplate !== BuiltinFilterTemplates . DateRange ) { newSelectedFilter . operator = FilterConditionOperator . OR ; } const values = newSelectedFilter . values . filter ( selectedValue => { return selectedValue . selected ; } ) ; if ( values . length > 0 ) { newSelectedFilter . values = values . map ( value => { let newValue = cloneDeep ( value ) ; delete newValue . selected ; delete newValue . selectedOnce ; delete newValue . count ; if ( ! newValue . operator ) newValue . operator = FilterComparisonOperator . Eq ; return newValue ; } ) ; delete newSelectedFilter . expandByDefault ; delete newSelectedFilter . hasSelectedValues ; delete newSelectedFilter . selectedOnce ; delete newSelectedFilter . showCount ; delete newSelectedFilter . isMulti ; delete newSelectedFilter . displayName ; delete newSelectedFilter . canApply ; delete newSelectedFilter . canClear ; delete newSelectedFilter . sortIdx ; delete newSelectedFilter . selectedTemplate ; return newSelectedFilter ; } } ) ; return selectedFilters . filter ( filter => filter ) ; } /**
     * Binds event fired from filter value web components ('When an single filter value is selected')
     */ private bindFilterEvents ( ) { this . componentRef . current . addEventListener ( ExtensibilityConstants . EVENT_FILTER_UPDATED , ( ( ev ) => { ev . stopImmediatePropagation ( ) ; const dataFilterInfo = ev . detail as IDataFilterInfo ; this . onFilterValuesUpdated ( dataFilterInfo ) ; } ) . bind ( this ) ) ; } /**
     * Binds event fired from filter value web components ('When all filter values are applied (multi values filter)')
     */ private bindApplyFiltersEvents ( ) { this . componentRef . current . addEventListener ( ExtensibilityConstants . EVENT_FILTER_APPLY_ALL , ( ( ev ) => { ev . stopImmediatePropagation ( ) ; const submittedFilters = this . getSelectedFiltersFromUIFilters ( this . state . currentUiFilters ) ; this . setFiltersDeepLink ( submittedFilters ) ; this . getFiltersToDisplay ( this . props . availableFilters , this . state . currentUiFilters , this . props . filtersConfiguration ) ; this . setState ( { submittedFilters : submittedFilters } ) ; this . props . onUpdateFilters ( submittedFilters ) ; } ) . bind ( this ) ) ; } /**
     * Binds event fired from filter value web components ('When all filter values are cleared (multi values filter)')
     */ private bindClearFiltersEvents ( ) { this . componentRef . current . addEventListener ( ExtensibilityConstants . EVENT_FILTER_CLEAR_ALL , ( ( ev ) => { ev . stopImmediatePropagation ( ) ; const updatedfilters = this . state . currentUiFilters . map ( selectedFilter => { const updatedFilter = cloneDeep ( selectedFilter ) ; if ( updatedFilter . filterName === ev . detail . filterName ) { updatedFilter . values = [ ] ; updatedFilter . selectedOnce = true ; updatedFilter . hasSelectedValues = false ; } else { updatedFilter . values = updatedFilter . values . filter ( filter => filter . selected ) ; } return updatedFilter ; } ) ; const updateSubmittedFilters = this . state . submittedFilters . map ( submittedFilter => { if ( submittedFilter . filterName === ev . detail . filterName ) { submittedFilter . values = [ ] ; } return submittedFilter ; } ) ; this . setState ( { submittedFilters : updateSubmittedFilters } ) ; this . getFiltersToDisplay ( this . props . availableFilters , updatedfilters , this . props . filtersConfiguration ) ; this . resetFiltersDeepLink ( ) ; this . props . onUpdateFilters ( updateSubmittedFilters ) ; } ) . bind ( this ) ) ; } /**
     * Binds events fired from the filter operator components
     */ private bindFilterValueOperatorUpdated ( ) { this . componentRef . current . addEventListener ( ExtensibilityConstants . EVENT_FILTER_VALUE_OPERATOR_UPDATED , ( ( ev ) => { ev . stopImmediatePropagation ( ) ; const filters = this . state . currentUiFilters . map ( filter => { const selectedValues = filter . values . filter ( v => v . selected ) ; const submittedValues = this . state . submittedFilters . filter ( f => f . filterName === ev . detail . filterName && f . values . filter ( v => selectedValues . map ( s => s . value ) . indexOf ( v . value ) !== - 1 ) ) ; if ( filter . filterName === ev . detail . filterName ) { filter . canApply = ( ! filter . canApply && filter . operator !== ev . detail . operator && selectedValues . length > 1 ) || ( filter . canApply && submittedValues . length === 0 ) ; filter . operator = ev . detail . operator ; } return filter ; } ) ; this . setState ( { currentUiFilters : filters } ) ; } ) . bind ( this ) ) ; } private getTemplateContext ( ) { return { filters : this . state . currentUiFilters , selectedFilters : this . state . submittedFilters , instanceId : this . props . instanceId , theme : this . props . themeVariant , strings : commonStrings . Filters , selectedOnce : this . state . currentUiFilters . filter ( currentFilter => currentFilter . selectedOnce ) . length > 0 , properties : { ... this . props . properties } , } ; } /**
     * Retrieves the default filters from the URL and set them as initial state
     */ private getFiltersDeepLink ( ) { const queryString = UrlHelper . getQueryStringParam ( DEEPLINK_QUERYSTRING_PARAM , window . location . href ) ; if ( queryString ) { try { const dataFilters = JSON . parse ( decodeURIComponent ( queryString ) ) ; const currentUiFilters = dataFilters . map ( filter => { const filterConfiguration = DataFilterHelper . getConfigurationForFilter ( filter , this . props . filtersConfiguration ) ; return { displayName : filterConfiguration . displayValue ? filterConfiguration . displayValue : filter . filterName , expandByDefault : filterConfiguration . expandByDefault , filterName : filter . filterName , isMulti : filterConfiguration . isMulti , selectedTemplate : filterConfiguration . selectedTemplate , showCount : filterConfiguration . showCount , selectedOnce : true , operator : filter . operator , values : filter . values . map ( ( value ) => { value . selected = true ; value . selectedOnce = true ; return value ; } ) , canApply : false , canClear : true } as IDataFilterInternal ; } ) ; this . props . onUpdateFilters ( dataFilters ) ; this . setState ( { currentUiFilters : currentUiFilters , submittedFilters : dataFilters } ) ; } catch ( e ) { Log . verbose ( `[SearchFiltersContainer.getFiltersDeepLink]` , `Filters format in the query string is invalid.` ) ; } } } param submittedFilters private setFiltersDeepLink ( submittedFilters ) { let filtersDeepLinkUrl ; if ( submittedFilters . length > 0 ) { filtersDeepLinkUrl = UrlHelper . addOrReplaceQueryStringParam ( window . location . href , DEEPLINK_QUERYSTRING_PARAM , JSON . stringify ( submittedFilters ) ) ; } else { filtersDeepLinkUrl = UrlHelper . removeQueryStringParam ( DEEPLINK_QUERYSTRING_PARAM , window . location . href ) ; } window . history . pushState ( { path : filtersDeepLinkUrl } , '' , filtersDeepLinkUrl ) ; } private resetFiltersDeepLink ( ) { const filtersDeepLinkUrl = UrlHelper . removeQueryStringParam ( DEEPLINK_QUERYSTRING_PARAM , window . location . href ) ; window . history . pushState ( { path : filtersDeepLinkUrl } , '' , filtersDeepLinkUrl ) ; } /**
     * Subscribes to URL query string change events using SharePoint page router
     */ private _handleQueryStringChange ( ) { window . onpopstate = ( ev ) => { const queryString = UrlHelper . getQueryStringParam ( DEEPLINK_QUERYSTRING_PARAM , window . location . href ) ; if ( ! queryString ) { this . setState ( { currentUiFilters : this . resetSelectedFilterValues ( this . state . currentUiFilters ) , submittedFilters : [ ] } ) ; this . props . onUpdateFilters ( [ ] ) ; } this . getFiltersDeepLink ( ) ; } ; } private resetSelectedFilterValues ( currentUiFilters ) { const updatedfilters = currentUiFilters . map ( selectedFilter => { const updatedFilter = cloneDeep ( selectedFilter ) ; updatedFilter . values = [ ] ; return updatedFilter ; } ) ; return updatedfilters ; } }