import * as React from 'react' ; import { PrimaryButton , DefaultButton , IconButton } from 'office-ui-fabric-react/lib/Button' ; import { Panel , PanelType } from 'office-ui-fabric-react/lib/Panel' ; import { Spinner , SpinnerSize } from 'office-ui-fabric-react/lib/Spinner' ; import { Autofill } from 'office-ui-fabric-react/lib/components/Autofill/Autofill' ; import { Label } from 'office-ui-fabric-react/lib/Label' ; import TermPicker from './TermPicker' ; import { IPickerTerms , IPickerTerm } from './ITermPicker' ; import { ITaxonomyPickerProps , ITaxonomyPickerState } from './ITaxonomyPicker' ; import SPTermStorePickerService from './../../services/SPTermStorePickerService' ; import { ITermSet , ITerm } from './../../services/ISPTermStorePickerService' ; import * as strings from 'ControlStrings' ; import styles from './TaxonomyPicker.module.scss' ; import { sortBy , cloneDeep , isEqual } from '@microsoft/sp-lodash-subset' ; import uniqBy from 'lodash/uniqBy' ; import TermParent from './TermParent' ; import FieldErrorMessage from '../errorMessage/ErrorMessage' ; import { initializeIcons } from '@uifabric/icons' ; import * as telemetry from '../../common/telemetry' ; import { EmptyGuid } from '../../common/Constants' ; /**
 * Image URLs / Base64
 */ export const COLLAPSED_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAUCAYAAABSx2cSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABh0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjEwcrIlkgAAAIJJREFUOE/NkjEKwCAMRdu7ewZXJ/EqHkJwE9TBCwR+a6FLUQsRwYBTeD8/35wADnZVmPvY4OOYO3UNbK1FKeUWH+fRtK21hjEG3vuhQBdOKUEpBedcV6ALExFijJBSIufcFBjCVSCEACEEqpNvBmsmT+3MTnvqn/+O4+1vdtv7274APmNjtuXVz6sAAAAASUVORK5CYII=' ; export const EXPANDED_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAUCAYAAABSx2cSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABh0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjEwcrIlkgAAAFtJREFUOE9j/P//PwPZAKSZXEy2RrCLybV1CGjetWvX/46ODqBLUQOXoJ9BGtXU1MCYJM0wjZGRkaRpRtZIkmZ0jSRpBgUOzJ8wmqwAw5eICIb2qGYSkyfNAgwAasU+UQcFvD8AAAAASUVORK5CYII=' ; export const GROUP_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC9SURBVDhPY2CgNXh1qEkdiJ8D8X90TNBuJM0V6IpBhoHFgIxebKYTIwYzAMNpxGhGdsFwNoBgNEFjAWsYgOSKiorMgPgbEP/Hgj8AxXpB0Yg1gQAldYuLix8/efLkzn8s4O7du9eAan7iM+DV/v37z546der/jx8/sJkBdhVOA5qbm08ePnwYrOjQoUOkGwDU+AFowLmjR4/idwGukAYaYAkMgxfPnj27h816kDg4DPABoAI/IP6DIxZA4l0AOd9H3QXl5+cAAAAASUVORK5CYII=' ; export const TERMSET_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACaSURBVDhPrZLRCcAgDERdpZMIjuQA7uWH4CqdxMY0EQtNjKWB0A/77sxF55SKMTalk8a61lqCFqsLiwKac84ZRUUBi7MoYHVmAfjfjzE6vJqZQfie0AcwBQVW8ATi7AR7zGGGNSE6Q2cyLSPIjRswjO7qKhcPDN2hK46w05wZMcEUIG+HrzzcrRsQBIJ5hS8C9fGAPmRwu/9RFxW6L8CM4Ry8AAAAAElFTkSuQmCC' ; export const TERM_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACzSURBVDhPY2AYNKCoqIgTiOcD8X8S8F6wB4Aa1IH4akNDw+mPHz++/E8EuHTp0jmQRSDNCcXFxa/XrVt3gAh9KEpgBvx/9OjRLVI1g9TDDYBp3rlz5//Kysr/IJoYgGEASPPatWsbQDQxAMOAbdu2gZ0FookBcAOePHlyhxgN6GqQY+Hdhg0bDpJqCNgAaDrQAnJuNDY2nvr06dMbYgw6e/bsabgBUEN4yEiJ2wdNViLfIQC3sTh2vtJcswAAAABJRU5ErkJggg==' ; initializeIcons ( ) ; /**
 * Renders the controls for PropertyFieldTermPicker component
 */ export class TaxonomyPicker extends React . Component < ITaxonomyPickerProps , ITaxonomyPickerState > { private termsService : SPTermStorePickerService ; private previousValues : IPickerTerms = [ ] ; private invalidTerm : string = null ; private cancel : boolean = true ; /**
     * Constructor method
     */ constructor ( props ) { super ( props ) ; telemetry . track ( 'ReactTaxonomyPicker' ) ; this . state = { activeNodes : this . props . initialValues || [ ] , termSetAndTerms : null , loaded : false , openPanel : false , errorMessage : props . errorMessage } ; this . onOpenPanel = this . onOpenPanel . bind ( this ) ; this . onClosePanel = this . onClosePanel . bind ( this ) ; this . onSave = this . onSave . bind ( this ) ; this . termsChanged = this . termsChanged . bind ( this ) ; this . termsFromPickerChanged = this . termsFromPickerChanged . bind ( this ) ; this . onInputChange = this . onInputChange . bind ( this ) ; this . onBlur = this . onBlur . bind ( this ) ; this . termsService = new SPTermStorePickerService ( this . props , this . props . context ) ; } /**
     * componentDidMount lifecycle hook
     */ public componentDidMount ( ) { this . validateTerms ( ) ; } /**
     * componentWillMount lifecycle hook
     */ public componentWillMount ( ) { this . setState ( { activeNodes : this . props . initialValues || [ ] } ) ; } public componentWillReceiveProps ( nextProps ) { let newState ; if ( ! isEqual ( this . props . initialValues , nextProps . initialValues ) ) { newState = { activeNodes : nextProps . initialValues || [ ] } ; } if ( nextProps . errorMessage !== this . props . errorMessage ) { if ( ! newState ) { newState = { } ; } newState . errorMessage = nextProps . errorMessage ; } if ( newState ) { this . setState ( newState ) ; } } /**
    * it checks, if all entries still exist in term store. if allowMultipleSelections is true. it have to validate all values
    */ private async validateTerms ( ) { const { hideDeprecatedTags , hideTagsNotAvailableForTagging , initialValues , validateOnLoad , termsetNameOrID , useSessionStorage } = this . props ; let isValidateOnLoad = validateOnLoad && initialValues && initialValues . length >= 1 ; if ( isValidateOnLoad ) { const notFoundTerms = [ ] ; const notFoundTermIds = [ ] ; const termSet = await this . termsService . getAllTerms ( termsetNameOrID , hideDeprecatedTags , hideTagsNotAvailableForTagging , useSessionStorage ) ; const allTerms = termSet . Terms ; for ( let i = 0 , len = initialValues . length ; i < len ; i ++ ) { const pickerTerm = initialValues [ i ] ; if ( ! allTerms . filter ( t => t . Id === pickerTerm . key ) . length ) { notFoundTerms . push ( pickerTerm . name ) ; notFoundTermIds . push ( pickerTerm . key ) ; } } if ( notFoundTerms . length ) { this . setState ( { internalErrorMessage : strings . TaxonomyPickerTermsNotFound . replace ( '{0}' , notFoundTerms . join ( ', ' ) ) , invalidNodeIds : notFoundTermIds } ) ; } } } /**
     * Loads the list from SharePoint current web site
     */ private loadTermStores ( ) { if ( this . props . termActions && this . props . termActions . initialize ) { this . props . termActions . initialize ( this . termsService ) ; } this . termsService . getAllTerms ( this . props . termsetNameOrID , this . props . hideDeprecatedTags , this . props . hideTagsNotAvailableForTagging , this . props . useSessionStorage ) . then ( ( response ) => { let termSetAndTerms = response ? response : null ; this . setState ( { termSetAndTerms , loaded : true } ) ; } ) ; } /**
     * Force update of the taxonomy tree - required by term action in case the term has been added, deleted or moved.
     */ private async updateTaxonomyTree ( ) { const termSetAndTerms = await this . termsService . getAllTerms ( this . props . termsetNameOrID , this . props . hideDeprecatedTags , this . props . hideTagsNotAvailableForTagging , this . props . useSessionStorage ) ; this . setState ( { termSetAndTerms } ) ; } /**
     * Open the right Panel
     */ private onOpenPanel ( ) { if ( this . props . disabled === true ) { return ; } this . previousValues = cloneDeep ( this . state . activeNodes ) ; this . cancel = true ; this . loadTermStores ( ) ; this . setState ( { openPanel : true , loaded : false } ) ; } /**
     * Close the panel
     */ private onClosePanel ( ) { this . setState ( ( ) => { const newState = { openPanel : false , loaded : false } ; if ( this . cancel ) { newState . activeNodes = this . previousValues ; } return newState ; } ) ; } /**
     * On save click action
     */ private onSave ( ) { this . cancel = false ; this . onClosePanel ( ) ; this . validate ( this . state . activeNodes ) ; } param node private termsChanged ( term , checked ) { let activeNodes = this . state . activeNodes . slice ( ) ; if ( typeof term === 'undefined' || term === null ) { return ; } const { allowMultipleSelections , selectChildrenIfParentSelected } = this . props ; const { termSetAndTerms } = this . state ; const termItem = { name : term . Name , key : term . Id , path : term . PathOfTerm , termSet : term . TermSet . Id } ; let children = [ ] ; if ( allowMultipleSelections && selectChildrenIfParentSelected ) { if ( term . Id === term . TermSet . Id ) { children = termSetAndTerms . Terms || [ ] ; } else { children = termSetAndTerms . Terms ? termSetAndTerms . Terms . filter ( t => { return t . PathOfTerm . indexOf ( `${ term . PathOfTerm };` ) !== - 1 ; } ) : [ ] ; } } if ( checked ) { if ( allowMultipleSelections ) { activeNodes . push ( termItem ) ; } else { activeNodes = [ termItem ] ; } if ( children . length ) { activeNodes . push ( ... children . map ( c => { return { name : c . Name , key : c . Id , path : c . PathOfTerm , termSet : c . TermSet . Id } ; } ) ) ; } activeNodes = uniqBy ( activeNodes , 'key' ) ; } else { activeNodes = activeNodes . filter ( item => item . key !== term . Id ) ; if ( children . length ) { const childIds = children . map ( c => c . Id ) ; activeNodes = activeNodes . filter ( item => childIds . indexOf ( item . key ) === - 1 ) ; } } activeNodes = sortBy ( activeNodes , 'path' ) ; if ( this . props . onPanelSelectionChange ) { this . props . onPanelSelectionChange ( this . state . activeNodes . slice ( ) , activeNodes ) ; } this . setState ( { activeNodes : activeNodes } ) ; } param node private termsFromPickerChanged ( terms ) { this . setState ( { activeNodes : terms } ) ; this . validate ( terms ) ; } /**
     * Shows an error message for any invalid input inside taxonomy picker control
     */ private validateInputText ( ) { if ( ! ! this . invalidTerm ) { this . setState ( { errorMessage : strings . TaxonomyPickerInvalidTerms . replace ( '{0}' , this . invalidTerm ) } ) ; } else { this . setState ( { errorMessage : null } ) ; } } /**
     * Triggers when input of taxonomy picker control changes
     */ private onInputChange ( input ) { if ( ! input ) { const { validateInput } = this . props ; if ( ! ! validateInput ) { this . invalidTerm = null ; this . validateInputText ( ) ; } } return input ; } private async validateOnGetErrorMessage ( targetValue ) { const errorMessage = await this . props . onGetErrorMessage ( [ { key : EmptyGuid , name : targetValue , path : targetValue , termSet : this . termsService . cleanGuid ( this . props . termsetNameOrID ) } ] ) ; if ( ! ! errorMessage ) { this . setState ( { errorMessage : errorMessage } ) ; } else { this . setState ( { errorMessage : null } ) ; } return ! errorMessage ; } private onNewTerm = ( newLabel ) => { this . props . onNewTerm ( { key : EmptyGuid , name : newLabel , path : newLabel , termSet : this . termsService . cleanGuid ( this . props . termsetNameOrID ) } ) ; } ; /**
     * Triggers when taxonomy picker control loses focus
     */ private async onBlur ( event ) { const { validateInput } = this . props ; if ( ! ! validateInput ) { const target = event . target as HTMLInputElement ; const targetValue = ! ! target ? target . value : null ; if ( ! ! this . props . onGetErrorMessage && ! ! targetValue ) { await this . validateOnGetErrorMessage ( targetValue ) ; } else { if ( ! ! targetValue ) { this . invalidTerm = targetValue ; } else { this . invalidTerm = null ; } this . validateInputText ( ) ; } } } param node private getSelectedNodePosition ( node ) { for ( let i = 0 ; i < this . state . activeNodes . length ; i ++ ) { if ( node . key === this . state . activeNodes [ i ] . key ) { return i ; } } return - 1 ; } param termSet param isChecked private termSetSelectedChange = ( termSet , isChecked ) => { const ts = { ... termSet } ; ts . Id = this . termsService . cleanGuid ( ts . Id ) ; const term = { Name : ts . Name , Id : ts . Id , TermSet : ts , PathOfTerm : "" , _ObjectType_ : ts . _ObjectType_ , _ObjectIdentity_ : ts . _ObjectIdentity_ , Description : ts . Description , IsDeprecated : null , IsAvailableForTagging : null , IsRoot : null } ; this . termsChanged ( term , isChecked ) ; } ; private validate = async ( value ) : Promise < void > => { if ( this . state . invalidNodeIds ) { const changedInvalidNodeIds = this . state . invalidNodeIds . filter ( id => { return ! ! value . filter ( term => term . key === id ) . length ; } ) ; let internalErrorMessage = changedInvalidNodeIds . length ? this . state . internalErrorMessage : '' ; this . setState ( { invalidNodeIds : changedInvalidNodeIds , internalErrorMessage : internalErrorMessage } ) ; } if ( this . props . errorMessage || ! this . props . onGetErrorMessage ) { this . validated ( value ) ; return ; } const result = this . props . onGetErrorMessage ( value || [ ] ) ; if ( ! result ) { this . validated ( value ) ; return ; } if ( typeof result === 'string' ) { if ( ! result ) { this . validated ( value ) ; this . setState ( { errorMessage : undefined } ) ; } else { this . setState ( { errorMessage : result } ) ; } } else { try { const resolvedResult = await result ; if ( ! resolvedResult ) { this . validated ( value ) ; this . setState ( { errorMessage : undefined } ) ; } else { this . setState ( { errorMessage : resolvedResult } ) ; } } catch ( err ) { this . validated ( value ) ; } } } ; private validated = ( value ) : void => { this . props . onChange ( value ) ; } ; /**
     * Renders the SPListpicker controls with Office UI  Fabric
     */ public render ( ) { const { label , context , disabled , isTermSetSelectable , allowMultipleSelections , disabledTermIds , disableChildrenOfDisabledParents , placeholder , panelTitle , anchorId , termActions , required } = this . props ; const { activeNodes , errorMessage , internalErrorMessage , openPanel , loaded , termSetAndTerms } = this . state ; return ( < div > { label && < Label required = { required } > { label } < /Label>} < div className = { styles . termField } > < div className = { styles . termFieldInput } > < TermPicker context = { context } termPickerHostProps = { this . props } disabled = { disabled } value = { activeNodes } isTermSetSelectable = { isTermSetSelectable } onChanged = { this . termsFromPickerChanged } onInputChange = { this . onInputChange } onBlur = { this . onBlur } onNewTerm = { this . props . onNewTerm ? this . onNewTerm : undefined } allowMultipleSelections = { allowMultipleSelections } disabledTermIds = { disabledTermIds } disableChildrenOfDisabledParents = { disableChildrenOfDisabledParents } placeholder = { placeholder } / > < /div> < div className = { styles . termFieldButton } > < IconButton disabled = { disabled } iconProps = { { iconName : 'Tag' } } onClick = { this . onOpenPanel } / > < /div> < /div> < FieldErrorMessage errorMessage = { errorMessage || internalErrorMessage } /> < Panel isOpen = { openPanel } hasCloseButton = { true } onDismiss = { this . onClosePanel } isLightDismiss = { true } type = { PanelType . medium } headerText = { panelTitle } onRenderFooterContent = { ( ) => { return ( < div className = { styles . actions } > < PrimaryButton iconProps = { { iconName : 'Save' } } text = { strings . SaveButtonLabel } value = "Save" onClick = { this . onSave } / > < DefaultButton iconProps = { { iconName : 'Cancel' } } text = { strings . CancelButtonLabel } value = "Cancel" onClick = { this . onClosePanel } / > < /div>); } } > { loaded === false ? < Spinner size = { SpinnerSize . medium } /> : ''} { loaded === true && termSetAndTerms && ( < div key = { termSetAndTerms . Id } > < h3 > { termSetAndTerms . Name } < /h3> < TermParent anchorId = { anchorId } autoExpand = { null } termset = { termSetAndTerms } isTermSetSelectable = { isTermSetSelectable } termSetSelectedChange = { this . termSetSelectedChange } activeNodes = { activeNodes } disabledTermIds = { disabledTermIds } disableChildrenOfDisabledParents = { disableChildrenOfDisabledParents } changedCallback = { this . termsChanged } multiSelection = { allowMultipleSelections } spTermService = { this . termsService } updateTaxonomyTree = { this . updateTaxonomyTree } termActions = { termActions } / > < /div>)} < /Panel> < /div>); } }