/**
 * DISCLAIMER
 *
 * As there is not yet an OData end-point for managed metadata, this service makes use of the ProcessQuery end-points.
 * The service will get updated once the APIs are in place for managing managed metadata.
 */ import { SPHttpClient , SPHttpClientResponse , ISPHttpClientOptions } from '@microsoft/sp-http' ; import { Environment , EnvironmentType } from '@microsoft/sp-core-library' ; import { BaseComponentContext } from '@microsoft/sp-component-base' ; import { ITaxonomyPickerProps } from '../controls/taxonomyPicker/ITaxonomyPicker' ; import { IPickerTerm } from '../controls/taxonomyPicker/ITermPicker' ; import { ITermStore , ITerms , ITerm , IGroup , ITermSet , ISuggestTerm } from './ISPTermStorePickerService' ; import SPTermStoreMockHttpClient from './SPTermStorePickerMockService' ; import { findIndex } from '@microsoft/sp-lodash-subset' ; import { EmptyGuid } from '../common/Constants' ; import { LocalesHelper } from '../common/utilities/LocalesHelper' ; /**
 * Service implementation to manage term stores in SharePoint
 */ export default class SPTermStorePickerService { private taxonomySession : string ; private formDigest : string ; private clientServiceUrl : string ; private suggestionServiceUrl : string ; /**
     * Service constructor
     */ constructor ( private props , private context ) { if ( Environment . type !== EnvironmentType . Local ) { { this . clientServiceUrl = this . context . pageContext . web . absoluteUrl + '/_vti_bin/client.svc/ProcessQuery' ; this . suggestionServiceUrl = this . context . pageContext . web . absoluteUrl + "/_vti_bin/TaxonomyInternalService.json/GetSuggestions" ; } } } public async getTermLabels ( termId ) { if ( Environment . type === EnvironmentType . Local ) { return null ; } let result = null ; try { const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultKeywordsTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${ termId }</Parameter></Parameters></Method></ObjectPaths></Request>` ; const reqHeaders = new Headers ( ) ; reqHeaders . append ( "accept" , "application/json" ) ; reqHeaders . append ( "content-type" , "application/xml" ) ; const httpPostOptions = { headers : reqHeaders , body : data } ; const callResult = await this . context . spHttpClient . post ( this . clientServiceUrl , SPHttpClient . configurations . v1 , httpPostOptions ) ; const jsonResult = await callResult . json ( ) ; let node = jsonResult . find ( x => x . _ObjectType_ == "SP.Taxonomy.Term" ) ; if ( node && node . Labels && node . Labels . _Child_Items_ ) { result = node . Labels . _Child_Items_ . map ( termLabel => termLabel . Value ) ; } } catch ( error ) { result = null ; console . log ( error . message ) ; } return result ; } /**
     * Gets the collection of term stores in the current SharePoint env
     */ public getTermStores ( ) { if ( Environment . type === EnvironmentType . Local ) { return this . getTermStoresFromMock ( ) ; } else { const data = '<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><Query Id="7" ObjectPathId="4"><Query SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /><Property Name="Groups"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="IsSystemGroup" ScalarProperty="true" /><Property Name="TermSets"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="Description" ScalarProperty="true" /><Property Name="Names" ScalarProperty="true" /></Properties></ChildItemQuery></Property></Properties></ChildItemQuery></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /></ObjectPaths></Request>' ; const reqHeaders = new Headers ( ) ; reqHeaders . append ( "accept" , "application/json" ) ; reqHeaders . append ( "content-type" , "application/xml" ) ; const httpPostOptions = { headers : reqHeaders , body : data } ; return this . context . spHttpClient . post ( this . clientServiceUrl , SPHttpClient . configurations . v1 , httpPostOptions ) . then ( ( serviceResponse ) => { return serviceResponse . json ( ) . then ( ( serviceJSONResponse ) => { let termStoreResult = serviceJSONResponse . filter ( ( r ) => r [ '_ObjectType_' ] === 'SP.Taxonomy.TermStore' ) ; if ( termStoreResult . length > 0 ) { if ( this . props . termsetNameOrID ) { return termStoreResult . map ( termstore => { let termGroups = termstore . Groups . _Child_Items_ ; if ( this . props . termsetNameOrID ) { const termsetNameOrId = this . props . termsetNameOrID ; termGroups = termGroups . map ( ( group ) => { group . TermSets . _Child_Items_ = group . TermSets . _Child_Items_ . filter ( ( termSet ) => termSet . Name === termsetNameOrId || this . cleanGuid ( termSet . Id ) . toLowerCase ( ) === this . cleanGuid ( termsetNameOrId ) . toLowerCase ( ) ) ; return group ; } ) ; } termGroups = termGroups . filter ( group => ! group . IsSystemGroup ) ; termGroups = termGroups . filter ( ( group ) => group . TermSets . _Child_Items_ . length > 0 ) ; termstore . Groups . _Child_Items_ = termGroups ; return termstore ; } ) ; } return termStoreResult ; } return [ ] ; } ) ; } ) ; } } /**
     * Gets the current term set
     */ public async getTermSet ( ) { if ( Environment . type === EnvironmentType . Local ) { const termSetInfo = await SPTermStoreMockHttpClient . getAllTerms ( ) ; return termSetInfo ; } else { const termStore = await this . getTermStores ( ) ; return this . getTermSetId ( termStore , this . props . termsetNameOrID ) ; } } param termset public async getAllTerms ( termset , hideDeprecatedTags ? , hideTagsNotAvailableForTagging ? , useSessionStorage = true ) { if ( Environment . type === EnvironmentType . Local ) { return this . getAllMockTerms ( ) ; } else { let termsetId = termset ; if ( ! this . isGuid ( termset ) ) { const termStore = await this . getTermStores ( ) ; const crntTermSet = this . getTermSetId ( termStore , termset ) ; if ( crntTermSet ) { termsetId = this . cleanGuid ( crntTermSet . Id ) ; } else { return null ; } } let childTerms = this . getTermsById ( termsetId , useSessionStorage ) ; if ( childTerms ) { return childTerms ; } const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultKeywordsTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${ termsetId }</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>` ; const reqHeaders = new Headers ( ) ; reqHeaders . append ( "accept" , "application/json" ) ; reqHeaders . append ( "content-type" , "application/xml" ) ; const httpPostOptions = { headers : reqHeaders , body : data } ; return this . context . spHttpClient . post ( this . clientServiceUrl , SPHttpClient . configurations . v1 , httpPostOptions ) . then ( ( serviceResponse ) => { return serviceResponse . json ( ) . then ( ( serviceJSONResponse ) => { const termStoreResultTermSets = serviceJSONResponse . filter ( ( r ) => r [ '_ObjectType_' ] === 'SP.Taxonomy.TermSet' ) ; if ( termStoreResultTermSets . length > 0 ) { let termStoreResultTermSet = termStoreResultTermSets [ 0 ] ; termStoreResultTermSet . Terms = [ ] ; const termStoreResultTerms = serviceJSONResponse . filter ( ( r ) => r [ '_ObjectType_' ] === 'SP.Taxonomy.TermCollection' ) ; if ( termStoreResultTerms . length > 0 ) { let terms = termStoreResultTerms [ 0 ] . _Child_Items_ ; if ( hideDeprecatedTags === true ) { terms = terms . filter ( d => d [ "IsDeprecated" ] === false ) ; } if ( hideTagsNotAvailableForTagging === true ) { terms = terms . filter ( d => d [ "IsAvailableForTagging" ] === true ) ; } terms = terms . map ( term => { if ( term . IsRoot ) { term . CustomSortOrderIndex = ( termStoreResultTermSet . CustomSortOrder ) ? termStoreResultTermSet . CustomSortOrder . split ( ":" ) . indexOf ( this . cleanGuid ( term . Id ) ) : - 1 ; } else { term . CustomSortOrderIndex = ( term [ "Parent" ] . CustomSortOrder ) ? term [ "Parent" ] . CustomSortOrder . split ( ":" ) . indexOf ( this . cleanGuid ( term . Id ) ) : - 1 ; } term . Id = this . cleanGuid ( term . Id ) ; term [ 'PathDepth' ] = term . PathOfTerm . split ( ';' ) . length ; term . TermSet = { Id : this . cleanGuid ( termStoreResultTermSet . Id ) , Name : termStoreResultTermSet . Name } ; if ( term [ "Parent" ] ) { term . ParentId = this . cleanGuid ( term [ "Parent" ] . Id ) ; } return term ; } ) ; if ( terms . length > 0 ) { terms = this . sortTerms ( terms ) ; termStoreResultTermSet . Terms = terms ; } } try { if ( useSessionStorage && window . sessionStorage ) { window . sessionStorage . setItem ( termsetId , JSON . stringify ( termStoreResultTermSet ) ) ; } } catch ( error ) { } return termStoreResultTermSet ; } return null ; } ) ; } ) ; } } param termstore param termset private getTermSetId ( termstore , termsetName ) { if ( termstore && termstore . length > 0 && termsetName ) { const ts = termstore [ 0 ] ; if ( ts . Groups && ts . Groups . _Child_Items_ ) { for ( const group of ts . Groups . _Child_Items_ ) { if ( group . TermSets && group . TermSets . _Child_Items_ ) { for ( const termSet of group . TermSets . _Child_Items_ ) { if ( termSet . Name === termsetName ) { return termSet ; } } } } } } return null ; } param searchText public searchTermsByName ( searchText ) { if ( Environment . type === EnvironmentType . Local ) { return SPTermStoreMockHttpClient . searchTermsByName ( searchText ) ; } else { return this . searchTermsByTermSet ( searchText ) ; } } private getTermsById ( termId , useSessionStorage = true ) { try { if ( useSessionStorage && window . sessionStorage ) { let terms = window . sessionStorage . getItem ( termId ) ; if ( terms ) { return JSON . parse ( terms ) ; } else { return null ; } } else { return null ; } } catch ( error ) { return null ; } } private searchTermsBySearchText ( terms , searchText ) { if ( terms ) { return terms . filter ( ( t ) => { return t . name . toLowerCase ( ) . indexOf ( searchText . toLowerCase ( ) ) > - 1 ; } ) ; } else return [ ] ; } public async searchTermsByTermId ( searchText , termId ) { if ( Environment . type === EnvironmentType . Local ) { return SPTermStoreMockHttpClient . searchTermsByName ( searchText ) ; } else { const { useSessionStorage } = this . props ; let childTerms = this . getTermsById ( termId , useSessionStorage ) ; if ( childTerms ) { return this . searchTermsBySearchText ( childTerms , searchText ) ; } else { const { termsetNameOrID , hideDeprecatedTags , hideTagsNotAvailableForTagging , } = this . props ; const terms = await this . getAllTermsByAnchorId ( termsetNameOrID , termId , hideDeprecatedTags , hideTagsNotAvailableForTagging , useSessionStorage ) ; if ( terms ) { return this . searchTermsBySearchText ( terms , searchText ) ; } } return null ; } } /**
     * Retrieve all terms for the given term set and anchorId
     */ public async getAllTermsByAnchorId ( termsetNameOrID , anchorId , hideDeprecatedTags ? , hideTagsNotAvailableForTagging ? , useSessionStorage = true ) { let returnTerms = [ ] ; if ( Environment . type === EnvironmentType . Local ) { let terms = await this . getAllMockTermsByAnchor ( ) ; terms . forEach ( term => { returnTerms . push ( this . convertTermToPickerTerm ( term ) ) ; } ) ; } else { const childTerms = this . getTermsById ( anchorId , useSessionStorage ) ; if ( childTerms ) { return childTerms ; } let termSet = await this . getAllTerms ( termsetNameOrID , hideDeprecatedTags , hideTagsNotAvailableForTagging ) ; let terms = termSet . Terms ; if ( anchorId ) { const anchorTerm = terms . filter ( t => t . Id . toLowerCase ( ) === anchorId . toLowerCase ( ) ) . shift ( ) ; if ( anchorTerm ) { const anchorTermPath = `${ anchorTerm . PathOfTerm };` ; const anchorTerms = terms . filter ( t => t . PathOfTerm . substring ( 0 , anchorTermPath . length ) === anchorTermPath && t . Id !== anchorTerm . Id ) ; anchorTerms . forEach ( term => { returnTerms . push ( this . convertTermToPickerTerm ( term ) ) ; } ) ; try { if ( useSessionStorage && window . sessionStorage ) { window . sessionStorage . setItem ( anchorId , JSON . stringify ( returnTerms ) ) ; } } catch ( error ) { } } } else { terms . forEach ( term => { returnTerms . push ( this . convertTermToPickerTerm ( term ) ) ; } ) ; } } return returnTerms ; } param searchText param termsetId private searchTermsByTermSet ( searchText ) { if ( Environment . type === EnvironmentType . Local ) { return SPTermStoreMockHttpClient . searchTermsByName ( searchText ) ; } else { return new Promise < IPickerTerm [ ] > ( resolve => { this . getTermStores ( ) . then ( termStore => { let TermSetId = this . props . termsetNameOrID ; if ( ! this . isGuid ( TermSetId ) ) { const crntTermSet = this . getTermSetId ( termStore , TermSetId ) ; if ( crntTermSet ) { TermSetId = this . cleanGuid ( crntTermSet . Id ) ; } else { resolve ( null ) ; return ; } } if ( termStore === undefined || termStore . length === 0 ) { resolve ( null ) ; return ; } let data = { start : searchText , lcid : LocalesHelper . getLocaleId ( this . context . pageContext . cultureInfo . currentUICultureName ) ?? this . context . pageContext . web . language , sspList : this . cleanGuid ( termStore [ 0 ] . Id ) , termSetList : TermSetId , anchorId : this . props . anchorId ? this . props . anchorId : EmptyGuid , isSpanTermStores : false , isSpanTermSets : false , isIncludeUnavailable : this . props . hideTagsNotAvailableForTagging === true , isIncludeDeprecated : this . props . hideDeprecatedTags === true , isAddTerms : false , isIncludePathData : false , excludeKeyword : false , excludedTermset : EmptyGuid } ; const reqHeaders = new Headers ( ) ; reqHeaders . append ( "accept" , "application/json" ) ; reqHeaders . append ( "content-type" , "application/json" ) ; const httpPostOptions = { headers : reqHeaders , body : JSON . stringify ( data ) } ; return this . context . spHttpClient . post ( this . suggestionServiceUrl , SPHttpClient . configurations . v1 , httpPostOptions ) . then ( ( serviceResponse ) => { return serviceResponse . json ( ) . then ( ( serviceJSONResponse ) => { const groups = serviceJSONResponse . d . Groups ; if ( groups && groups . length > 0 ) { const terms = groups [ 0 ] . Suggestions ; if ( terms . length > 0 ) { let returnTerms = terms . map ( ( term ) => this . convertSuggestTermToPickerTerm ( term , TermSetId ) ) ; resolve ( returnTerms ) ; return ; } } resolve ( [ ] ) ; } ) ; } ) ; } ) ; } ) ; } } private isGuid ( strGuid ) { return /^[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}$/ . test ( strGuid ) ; } param terms private sortTerms ( terms ) { let newTermsOrder = [ ] ; let itemsToSort = true ; let pathLevel = 1 ; while ( itemsToSort ) { let crntTerms = terms . filter ( term => term . PathDepth === pathLevel ) ; if ( crntTerms && crntTerms . length > 0 ) { crntTerms = crntTerms . sort ( this . sortTermByPath ) ; if ( pathLevel !== 1 ) { crntTerms = crntTerms . reverse ( ) ; for ( const crntTerm of crntTerms ) { const pathElms = crntTerm . PathOfTerm . split ( ";" ) ; pathElms . pop ( ) ; const idx = findIndex ( newTermsOrder , term => term . PathOfTerm === pathElms . join ( ";" ) ) ; if ( idx !== - 1 ) { newTermsOrder . splice ( idx + 1 , 0 , crntTerm ) ; } else { newTermsOrder . push ( crntTerm ) ; } } } else { newTermsOrder = crntTerms ; } ++ pathLevel ; } else { itemsToSort = false ; } } return newTermsOrder ; } param a param b private sortTermByPath ( a , b ) { if ( a . CustomSortOrderIndex === - 1 ) { if ( a . PathOfTerm . toLowerCase ( ) < b . PathOfTerm . toLowerCase ( ) ) { return - 1 ; } if ( a . PathOfTerm . toLowerCase ( ) > b . PathOfTerm . toLowerCase ( ) ) { return 1 ; } return 0 ; } else { if ( a . CustomSortOrderIndex < b . CustomSortOrderIndex ) { return - 1 ; } if ( a . CustomSortOrderIndex > b . CustomSortOrderIndex ) { return 1 ; } return 0 ; } } param guid public cleanGuid ( guid ) { if ( guid !== undefined ) { return guid . replace ( '/Guid(' , '' ) . replace ( '/' , '' ) . replace ( ')' , '' ) ; } else { return '' ; } } /**
     * Returns 3 fake SharePoint lists for the Mock mode
     */ private getTermStoresFromMock ( ) { return SPTermStoreMockHttpClient . getTermStores ( this . context . pageContext . web . absoluteUrl ) . then ( ( data ) => { return data ; } ) as Promise < ITermStore [ ] > ; } /**
     * Returns 3 fake SharePoint lists for the Mock mode
     */ private getAllMockTerms ( ) { return SPTermStoreMockHttpClient . getAllTerms ( ) . then ( ( data ) => { return data ; } ) as Promise < ITermSet > ; } /**
     * Returns 3 fake SharePoint lists for the Mock mode
     */ private getAllMockTermsByAnchor ( ) { return SPTermStoreMockHttpClient . getAllTermsByAnchorId ( ) . then ( ( data ) => { return data ; } ) as Promise < ITerm [ ] > ; } private convertTermToPickerTerm ( term ) { return { key : this . cleanGuid ( term . Id ) , name : term . Name , path : term . PathOfTerm , termSet : this . cleanGuid ( term . TermSet . Id ) , termSetName : term . TermSet . Name } ; } private convertSuggestTermToPickerTerm ( term , termSetId ) { let path = "" ; let termSetName = "" ; if ( term . Paths && term . Paths . length > 0 ) { const fullPath = term . Paths [ 0 ] . replace ( /^\[/ , "" ) . replace ( /\]$/ , "" ) ; const fullPathParts = fullPath . split ( ":" ) ; path = fullPathParts . join ( ";" ) + ";" + term . DefaultLabel ; termSetName = fullPathParts [ 0 ] ; } return { key : this . cleanGuid ( term . Id ) , name : term . DefaultLabel , path : path , termSet : termSetId ? termSetId : EmptyGuid , termSetName : termSetName } ; } }