import * as React from 'react' ; import { ISearchResultsContainerProps } from './ISearchResultsContainerProps' ; import { ISearchResultsContainerState } from './ISearchResultsContainerState' ; import { TemplateRenderer } from "../../../controls/TemplateRenderer/TemplateRenderer" ; import { Shimmer , ShimmerElementType as ElemType , ShimmerElementsGroup , SelectionZone , Selection , SelectionMode } from 'office-ui-fabric-react' ; import { isEqual , cloneDeep , merge , isEmpty } from "@microsoft/sp-lodash-subset" ; import { ITemplateService } from '../../../services/templateService/ITemplateService' ; import { TemplateService } from '../../../services/templateService/TemplateService' ; import { Log , DisplayMode } from "@microsoft/sp-core-library" ; import { MessageBar , MessageBarType , Overlay , Spinner , SpinnerSize } from 'office-ui-fabric-react' ; import { IDataSourceData , IDataFilterResult , BuiltinTemplateSlots } from '@pnp/modern-search-extensibility' ; import { IDataResultsTemplateContext } from '../../../models/common/ITemplateContext' ; import styles from './SearchResultsContainer.module.scss' ; import { Constants , AutoCalculatedDataSourceFields , TestConstants } from '../../../common/Constants' ; import { ITemplateSlot } from '@pnp/modern-search-extensibility' ; import { ObjectHelper } from '../../../helpers/ObjectHelper' ; import { BuiltinLayoutsKeys } from '../../../layouts/AvailableLayouts' ; import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle' ; import * as webPartStrings from 'SearchResultsWebPartStrings' ; const LogSource = "SearchResultsContainer" ; /**
 * Cached data structure for a data source
 */ export interface IDataSourceCacheData { data : IDataSourceData ; itemsCount : number ; } export default class SearchResultsContainer extends React . Component < ISearchResultsContainerProps , ISearchResultsContainerState > { /**
     * A template service instance
     */ private templateService : ITemplateService = undefined ; /**
     * Store the last available filters
     */ private _lastAvailableSearchFilters : IDataFilterResult [ ] = [ ] ; /**
     * The current total items count
     */ private _totalItemsCount : number = 0 ; /**
     * The current selection information in the template
     */ private _selection : Selection ; private _lastPageNumber : number ; private _lastPageSelectedKeys : string [ ] = [ ] ; public constructor ( props ) { super ( props ) ; this . state = { data : null , isLoading : true , errorMessage : '' , renderedOnce : false , selectedItemKeys : [ ] } ; this . templateService = this . props . serviceScope . consume < ITemplateService > ( TemplateService . ServiceKey ) ; this . _onSelectionChanged = this . _onSelectionChanged . bind ( this ) ; this . _selection = new Selection ( { onSelectionChanged : this . _onSelectionChanged , getKey : ( item , index ) => { return item . key = `${ this . props . dataContext . pageNumber }${ index }` ; } , } ) ; } public render ( ) { let renderTemplate = null ; let renderOverlay = null ; let templateContent = null ; let renderTitle = null ; let renderInfoMessage = null ; renderTitle = < div data - ui - test - id = { TestConstants . SearchResultsWebPartTitle } > < WebPartTitle displayMode = { this . props . webPartTitleProps . displayMode } title = { this . props . webPartTitleProps . title } updateProperty = { this . props . webPartTitleProps . updateProperty } moreLink = { this . props . webPartTitleProps . moreLink } themeVariant = { this . props . webPartTitleProps . themeVariant } className = { this . props . webPartTitleProps . className } / > < /div>; templateContent = this . templateService . getTemplateMarkup ( this . props . templateContent ) ; const templateContext = this . getTemplateContext ( ) ; let selectionMode = SelectionMode . none ; if ( this . props . properties . itemSelectionProps && this . props . properties . itemSelectionProps . allowItemSelection ) { selectionMode = this . props . properties . itemSelectionProps . allowMulti ? SelectionMode . multiple : SelectionMode . single ; } renderTemplate = < SelectionZone selection = { this . _selection } selectionMode = { selectionMode } > < TemplateRenderer templateContent = { templateContent } templateContext = { templateContext } templateService = { this . templateService } instanceId = { this . props . instanceId } / > < /SelectionZone>; if ( this . state . data && this . state . data . items . length === 0 ) { if ( this . props . properties . showBlankIfNoResult ) { if ( this . props . webPartTitleProps . displayMode === DisplayMode . Edit ) { if ( this . props . properties . selectedLayoutKey !== BuiltinLayoutsKeys . ResultsDebug ) { renderTemplate = < MessageBar messageBarType = { MessageBarType . info } > { webPartStrings . General . ShowBlankEditInfoMessage } < /MessageBar>; } } else { renderTitle = null ; renderTemplate = null ; } } } if ( this . state . isLoading ) { if ( this . state . renderedOnce ) { renderOverlay = < div data - ui - test - id = { TestConstants . SearchResultsLoadingOverlay } > < Overlay isDarkThemed = { false } className = { styles . overlay } > < Spinner size = { SpinnerSize . medium } /> < /Overlay> < /div>; } else { let renderShimmerElements = null ; templateContent = this . templateService . getPlaceholderMarkup ( this . props . templateContent ) ; if ( templateContent ) { renderShimmerElements = < TemplateRenderer templateContent = { templateContent } templateContext = { this . getTemplateContext ( ) } templateService = { this . templateService } instanceId = { this . props . instanceId } / > ; } else { renderShimmerElements = this . getDefaultShimmerElements ( ) ; } renderTemplate = renderShimmerElements ; } } let errorTemplate = null ; if ( this . state . errorMessage ) { errorTemplate = < div className = { TestConstants . SearchResultsErrorMessage } data - ui - test - id = { TestConstants . SearchResultsErrorMessage } > < MessageBar messageBarType = { MessageBarType . error } > { this . state . errorMessage } < /MessageBar></div > ; } return < main > < div data - instance - id = { this . props . instanceId } data - ui - test - id = { TestConstants . SearchResultsWebPart } > { renderOverlay } { renderInfoMessage } { renderTitle } { errorTemplate } { renderTemplate } < /div></main > ; } public async componentDidMount ( ) { await this . getDataFromDataSource ( this . props . dataContext . pageNumber ) ; } public async componentDidUpdate ( prevProps , prevState ) { if ( ! isEqual ( prevProps . dataSourceKey , this . props . dataSourceKey ) || ! isEqual ( prevProps . dataContext , this . props . dataContext ) || ! isEqual ( prevProps . properties . dataSourceProperties , this . props . properties . dataSourceProperties ) || ! isEqual ( prevProps . properties . templateSlots , this . props . properties . templateSlots ) ) { if ( ! isEqual ( prevProps . dataContext . pageNumber , this . props . dataContext . pageNumber ) ) { this . _lastPageSelectedKeys = this . _selection . getSelection ( ) . map ( item => item . key as string ) ; } await this . getDataFromDataSource ( this . props . dataContext . pageNumber ) ; } if ( ! this . props . properties . itemSelectionProps . allowItemSelection && this . state . data ) { this . _selection . setItems ( this . state . data . items , true ) ; } } param pageNumber private async getDataFromDataSource ( pageNumber ) { this . setState ( { isLoading : true , errorMessage : '' } ) ; this . forceUpdate ( ) ; try { let data = { items : [ ] } ; let availableFilters = [ ] ; let totalItemsCount = 0 ; let pageLinks = this . props . dataContext . paging . pageLinks ; let nextLinkUrl = this . props . dataContext . paging . nextLinkUrl ; const localDataContext = cloneDeep ( this . props . dataContext ) ; data = await this . props . dataSource . getData ( localDataContext ) ; data = await this . getItemsPreview ( data , this . convertTemplateSlotsToHashtable ( this . props . properties . templateSlots ) ) ; totalItemsCount = this . props . dataSource . getItemCount ( ) ; if ( totalItemsCount === 0 && data . paging && data . paging . links ) { data . paging . links = [ ] ; } if ( data . filters ) { if ( data . filters . length === 0 ) { availableFilters = this . _lastAvailableSearchFilters . map ( lastAvailableFilter => { lastAvailableFilter . values = [ ] ; return lastAvailableFilter ; } ) ; } else { availableFilters = data . filters ; this . _lastAvailableSearchFilters = availableFilters ; } } this . props . onDataRetrieved ( this . getAvailableFieldsFromResults ( data ) , availableFilters , pageNumber , nextLinkUrl , pageLinks ) ; this . _totalItemsCount = totalItemsCount ; this . setState ( { isLoading : false , data : data , renderedOnce : ! this . state . renderedOnce ? true : this . state . renderedOnce , } ) ; this . _selection . setItems ( cloneDeep ( data . items ) ) ; this . _lastPageNumber = pageNumber ; } catch ( error ) { this . setState ( { isLoading : false , errorMessage : error . message } ) ; Log . error ( LogSource , error , this . props . serviceScope ) ; throw error ; } } param data param slots private async getItemsPreview ( data , slots ) { const validPreviewExt = [ "SVG" , "MOVIE" , "PAGES" , "PICT" , "SKETCH" , "AI" , "PDF" , "PSB" , "PSD" , "3G2" , "3GP" , "ASF" , "BMP" , "HEVC" , "M2TS" , "M4V" , "MOV" , "MP3" , "MP4" , "MP4V" , "MTS" , "TS" , "WMV" , "DWG" , "FBX" , "ERF" , "ZIP" , "ZIP" , "DCM" , "DCM30" , "DICM" , "DICOM" , "PLY" , "HCP" , "GIF" , "HEIC" , "HEIF" , "JPEG" , "JPG" , "JPE" , "MEF" , "MRW" , "NEF" , "NRW" , "ORF" , "PANO" , "PEF" , "PNG" , "SPM" , "TIF" , "TIFF" , "XBM" , "XCF" , "KEY" , "LOG" , "CSV" , "DIC" , "DOC" , "DOCM" , "DOCX" , "DOTM" , "DOTX" , "POT" , "POTM" , "POTX" , "PPS" , "PPSM" , "PPSX" , "PPT" , "PPTM" , "PPTX" , "XD" , "XLS" , "XLSB" , "XLSX" , "SLTX" , "EML" , "MSG" , "VSD" , "VSDX" , "CUR" , "ICO" , "ICON" , "EPUB" , "ODP" , "ODS" , "ODT" , "ARW" , "CR2" , "CRW" , "DNG" , "RTF" , "ABAP" , "ADA" , "ADP" , "AHK" , "AS" , "AS3" , "ASC" , "ASCX" , "ASM" , "ASP" , "ASPX" , "AWK" , "BAS" , "BASH" , "BASH_LOGIN" , "BASH_LOGOUT" , "BASH_PROFILE" , "BASHRC" , "BAT" , "BIB" , "BSH" , "BUILD" , "BUILDER" , "C" , "CAPFILE" , "CBK" , "CC" , "CFC" , "CFM" , "CFML" , "CL" , "CLJ" , "CMAKE" , "CMD" , "COFFEE" , "CPP" , "CPT" , "CPY" , "CS" , "CSHTML" , "CSON" , "CSPROJ" , "CSS" , "CTP" , "CXX" , "D" , "DDL" , "DI.DIF" , "DIFF" , "DISCO" , "DML" , "DTD" , "DTML" , "EL" , "EMAKE" , "ERB" , "ERL" , "F90" , "F95" , "FS" , "FSI" , "FSSCRIPT" , "FSX" , "GEMFILE" , "GEMSPEC" , "GITCONFIG" , "GO" , "GROOVY" , "GVY" , "H" , "HAML" , "HANDLEBARS" , "HBS" , "HRL" , "HS" , "HTC" , "HTML" , "HXX" , "IDL" , "IIM" , "INC" , "INF" , "INI" , "INL" , "IPP" , "IRBRC" , "JADE" , "JAV" , "JAVA" , "JS" , "JSON" , "JSP" , "JSX" , "L" , "LESS" , "LHS" , "LISP" , "LOG" , "LST" , "LTX" , "LUA" , "M" , "MAKE" , "MARKDN" , "MARKDOWN" , "MD" , "MDOWN" , "MKDN" , "ML" , "MLI" , "MLL" , "MLY" , "MM" , "MUD" , "NFO" , "OPML" , "OSASCRIPT" , "OUT" , "P" , "PAS" , "PATCH" , "PHP" , "PHP2" , "PHP3" , "PHP4" , "PHP5" , "PL" , "PLIST" , "PM" , "POD" , "PP" , "PROFILE" , "PROPERTIES" , "PS" , "PS1" , "PT" , "PY" , "PYW" , "R" , "RAKE" , "RB" , "RBX" , "RC" , "RE" , "README" , "REG" , "REST" , "RESW" , "RESX" , "RHTML" , "RJS" , "RPROFILE" , "RPY" , "RSS" , "RST" , "RXML" , "S" , "SASS" , "SCALA" , "SCM" , "SCONSCRIPT" , "SCONSTRUCT" , "SCRIPT" , "SCSS" , "SGML" , "SH" , "SHTML" , "SML" , "SQL" , "STY" , "TCL" , "TEX" , "TEXT" , "TEXTILE" , "TLD" , "TLI" , "TMPL" , "TPL" , "TXT" , "VB" , "VI" , "VIM" , "WSDL" , "XAML" , "XHTML" , "XOML" , "XML" , "XSD" , "XSL" , "XSLT" , "YAML" , "YAWS" , "YML" , "ZSH" , "HTM" , "HTML" , "Markdown" , "MD" , "URL" ] ; if ( slots [ BuiltinTemplateSlots . PreviewUrl ] === AutoCalculatedDataSourceFields . AutoPreviewUrl ) { data . items = data . items . map ( item => { let contentClass = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . ContentClass ] ) ; const hasContentClass = ! isEmpty ( contentClass ) ; const isLibItem = hasContentClass && contentClass . indexOf ( "Library" ) !== - 1 ; let contentTypeId = this . props . dataSource . getTemplateSlots ( ) [ BuiltinTemplateSlots . IsFolder ] ; const isContainer = contentTypeId ? contentTypeId . indexOf ( '0x0120' ) !== - 1 : false ; let pathProperty = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . Path ] ) ; if ( ! pathProperty || ( hasContentClass && ! isLibItem ) ) { pathProperty = ObjectHelper . byPath ( item , BuiltinTemplateSlots . Path ) ; } if ( pathProperty && pathProperty . indexOf ( "?" ) === - 1 && ! isContainer ) { item . AutoPreviewUrl = pathProperty + "?web=1" ; } else { item . AutoPreviewUrl = pathProperty ; } return item ; } ) ; } if ( slots [ BuiltinTemplateSlots . PreviewImageUrl ] === AutoCalculatedDataSourceFields . AutoPreviewImageUrl ) { data . items = data . items . map ( item => { let contentClass = ObjectHelper . byPath ( item , BuiltinTemplateSlots . ContentClass ) ; if ( ! isEmpty ( contentClass ) && ( contentClass . toLocaleLowerCase ( ) == "sts_site" || contentClass . toLocaleLowerCase ( ) == "sts_web" ) ) { item [ AutoCalculatedDataSourceFields . AutoPreviewImageUrl ] = ObjectHelper . byPath ( item , "SiteLogo" ) ; } else { let siteId = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . SiteId ] ) ; let webId = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . WebId ] ) ; let listId = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . ListId ] ) ; let itemId = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . ItemId ] ) ; let isFolder = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . IsFolder ] ) ; const isContainerType = isFolder === "true" || isFolder === "1" || ( isFolder && isFolder . indexOf ( '0x0120' ) !== - 1 ) ; if ( siteId && listId && itemId && ! isContainerType ) { siteId = this . getGuidFromString ( siteId ) ; listId = this . getGuidFromString ( listId ) ; itemId = this . getGuidFromString ( itemId ) ; if ( webId ) { siteId = siteId + "," + this . getGuidFromString ( webId ) ; } const itemFileType = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . FileType ] ) ; if ( itemFileType && validPreviewExt . indexOf ( itemFileType . toUpperCase ( ) ) !== - 1 ) { item [ AutoCalculatedDataSourceFields . AutoPreviewImageUrl ] = `${ this . props . pageContext . site . absoluteUrl }/_api/v2.1/sites/${ siteId }/lists/${ listId }/items/${ itemId }/driveItem/thumbnails/0/large/content?preferNoRedirect=true` ; } } else { const driveId = ObjectHelper . byPath ( item , slots [ BuiltinTemplateSlots . DriveId ] ) ; if ( driveId && siteId && itemId ) { item [ AutoCalculatedDataSourceFields . AutoPreviewImageUrl ] = `${ this . props . pageContext . site . absoluteUrl }/_api/v2.1/sites/${ siteId }/drives/${ driveId }/items/${ itemId }/thumbnails/thumbnails/0/large/content?preferNoRedirect=true` ; } } } if ( ! this . isOnlineDomain ( item [ AutoCalculatedDataSourceFields . AutoPreviewImageUrl ] ) ) { item [ AutoCalculatedDataSourceFields . AutoPreviewImageUrl ] = null ; } return item ; } ) ; } return data ; } param domain private isOnlineDomain ( url ) { return ! isEmpty ( url ) && url . toLocaleLowerCase ( ) . indexOf ( window . location . hostname . split ( '.' ) . slice ( - 2 ) . join ( '.' ) . toLocaleLowerCase ( ) ) !== - 1 ; } param value private getGuidFromString ( value ) { if ( value ) { const matches = value . match ( /(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}/ ) ; if ( matches ) { return matches [ 0 ] ; } } return value ; } /**
     * Returns default shimmers if the template does not provide any
     */ private getDefaultShimmerElements ( ) { let i = 0 ; let renderShimmerElements = [ ] ; const shimmerContent = < div style = { { display : 'flex' } } > < ShimmerElementsGroup shimmerElements = { [ { type : ElemType . line , width : 40 , height : 40 } , { type : ElemType . gap , width : 10 , height : 40 } ] } /> < ShimmerElementsGroup flexWrap = { true } width = "100%" shimmerElements = { [ { type : ElemType . line , width : '100%' , height : 10 } , { type : ElemType . line , width : '75%' , height : 10 } , { type : ElemType . gap , width : '25%' , height : 20 } ] } / > < /div>; while ( i < 4 ) { renderShimmerElements . push ( < Shimmer key = { i } customElementsGroup = { shimmerContent } width = "100%" style = { { marginBottom : "20px" } } />); i ++ ; } return < div > { renderShimmerElements } < /div>; } private getTemplateContext ( ) { const { site , web , list , listItem , user , cultureInfo } = this . props . pageContext ; const trimmedProperties = cloneDeep ( this . props . properties ) ; delete trimmedProperties . filtersDataSourceReference ; delete trimmedProperties . inlineTemplateContent ; delete trimmedProperties . documentationLink ; delete trimmedProperties . externalTemplateUrl ; return { data : { totalItemsCount : this . _totalItemsCount , ... this . state . data } , paging : { currentPageNumber : this . props . dataContext . pageNumber } , theme : this . props . themeVariant , properties : { ... trimmedProperties } , filters : { selectedFilters : this . props . dataContext . filters . selectedFilters , filterOperator : this . props . dataContext . filters . filterOperator , instanceId : this . props . dataContext . filters . instanceId , filtersConfiguration : this . props . dataContext . filters . filtersConfiguration } , verticals : { selectedVertical : this . props . dataContext . verticals . selectedVertical } , inputQueryText : this . props . dataContext . inputQueryText , slots : this . convertTemplateSlotsToHashtable ( this . props . properties . templateSlots ) , context : { site : site , web : web , list : list , user : user , cultureInfo : cultureInfo , listItem : listItem } , instanceId : this . props . instanceId , utils : { defaultImage : Constants . DEFAULT_IMAGE_CONTENT } , selectedKeys : this . state . selectedItemKeys } ; } private convertTemplateSlotsToHashtable ( templateSlots ) { let slots = { } ; if ( templateSlots ) { templateSlots . forEach ( templateSlot => { slots [ templateSlot . slotName ] = templateSlot . slotField ; } ) ; } return slots ; } private getAvailableFieldsFromResults ( data ) { if ( data . items . length > 0 ) { let mergedItem = { } ; data . items . forEach ( item => { mergedItem = merge ( mergedItem , item ) ; } ) ; mergedItem = ObjectHelper . flatten ( mergedItem ) ; return Object . keys ( mergedItem ) . map ( key => { return key ; } ) ; } else { return [ ] ; } } private _onSelectionChanged ( ) { if ( this . props . dataContext . pageNumber === this . _lastPageNumber ) { const currentSelectedItems = this . _selection . getSelection ( ) ; const currentPageSelectionKeys = currentSelectedItems . map ( item => item . key as string ) ; this . props . onItemSelected ( currentSelectedItems ) ; this . setState ( { selectedItemKeys : [ ... this . _lastPageSelectedKeys , ... currentPageSelectionKeys ] } , ( ) => { this . forceUpdate ( ) ; } ) ; } } }