import * as React from 'react' ; import styles from '../PropertyFieldCollectionDataHost.module.scss' ; import { ICollectionDataItemProps , ICollectionDataItemState } from '.' ; import { TextField } from 'office-ui-fabric-react/lib/components/TextField' ; import { Icon } from 'office-ui-fabric-react/lib/components/Icon' ; import { Link } from 'office-ui-fabric-react/lib/components/Link' ; import * as strings from 'PropertyControlStrings' ; import { ICustomCollectionField , CustomCollectionFieldType , FieldValidator } from '..' ; import { Dropdown , IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown' ; import { Callout , DirectionalHint } from 'office-ui-fabric-react/lib/components/Callout' ; import { CollectionIconField } from '../collectionIconField' ; import { clone , findIndex , sortBy } from '@microsoft/sp-lodash-subset' ; import { CollectionNumberField } from '../collectionNumberField' ; import { CollectionColorField } from '../collectionColorField' ; import { Guid } from '@microsoft/sp-core-library' ; import { CollectionDropdownField } from '../collectionDropdownField/CollectionDropdownField' ; import { CollectionCheckboxField } from '../collectionCheckboxField/CollectionCheckboxField' ; export class CollectionDataItem extends React . Component < ICollectionDataItemProps , ICollectionDataItemState > { private emptyItem : any = null ; private validation : FieldValidator = { } ; private calloutCellRef : HTMLElement ; constructor ( props ) { super ( props ) ; let emptyItem = this . generateEmptyItem ( ) ; this . state = { crntItem : clone ( this . props . item ) || { ... emptyItem } , errorMsgs : [ ] , showCallout : false , disableAdd : false } ; } param prevProps param prevState public componentDidUpdate ( prevProps ) { if ( this . props . item !== prevProps . item ) { this . setState ( { crntItem : clone ( this . props . item ) } ) ; } } /**
     * Update the item value on the field change
     */ private onValueChanged = ( fieldId , value ) : Promise < void > => { return new Promise ( ( resolve ) => this . setState ( ( prevState ) => { const { crntItem } = prevState ; crntItem [ fieldId ] = value ; return { crntItem } ; } , ( ) => resolve ( ) ) ) ; } ; /**
     * Perform all required field checks at once
     */ private async doAllFieldChecks ( ) { const { crntItem } = this . state ; let disableAdd = null ; if ( this . props . fAddInCreation ) { if ( await this . checkRowIsValidForSave ( crntItem ) ) { disableAdd = true ; this . props . fAddInCreation ( crntItem , true ) ; } else { disableAdd = false ; this . props . fAddInCreation ( crntItem , false ) ; } } this . setState ( { disableAdd } ) ; if ( this . props . fUpdateItem ) { await this . updateItem ( ) ; } } /**
     * Check if all values of the required fields are provided
     */ private checkAllRequiredFieldsValid ( item ) { const requiredFields = this . props . fields . filter ( f => f . required ) ; for ( const field of requiredFields ) { if ( typeof item [ field . id ] === "undefined" || item [ field . id ] === null || item [ field . id ] === "" ) { return false ; } } return true ; } param item private checkAnyFieldContainsValue ( item ) { const { fields } = this . props ; for ( const field of fields ) { if ( typeof item [ field . id ] !== "undefined" && item [ field . id ] !== null && item [ field . id ] !== "" ) { return true ; } } return false ; } param item private async checkAnyFieldCustomErrorMessage ( item ) { const { fields , index } = this . props ; var validations = await Promise . all ( fields . filter ( f => f . onGetErrorMessage ) . map ( async ( f ) => { var validation = await f . onGetErrorMessage ( item [ f . id ] , index , item ) ; return this . storeFieldValidation ( f . id , validation ) ; } ) ) ; return validations . filter ( v => v && v . length > 0 ) . length == 0 ; } /**
     * Check if row is ready for save
     */ private async checkRowIsValidForSave ( item ) { return this . checkAllRequiredFieldsValid ( item ) && this . checkAnyFieldContainsValue ( item ) && await this . checkAnyFieldCustomErrorMessage ( item ) && this . checkAllFieldsAreValid ( ) ; } /**
     * Checks if all fields are valid
     */ private checkAllFieldsAreValid ( ) { if ( this . validation ) { const keys = Object . keys ( this . validation ) ; for ( const key of keys ) { if ( ! this . validation [ key ] ) { return false ; } } } return true ; } /**
     * Add the current row to the collection
     */ private addRow = async ( ) => { if ( this . props . fAddItem ) { const { crntItem } = this . state ; if ( this . checkRowIsValidForSave ( crntItem ) ) { this . props . fAddItem ( crntItem ) ; let emptyItem = this . generateEmptyItem ( ) ; this . setState ( { crntItem : { ... emptyItem } } ) ; } } } ; /**
     * Add the current row to the collection
     */ private updateItem = async ( ) => { const { crntItem } = this . state ; const isValid = await this . checkRowIsValidForSave ( crntItem ) ; if ( this . props . fUpdateItem ) { if ( isValid ) { this . props . fUpdateItem ( this . props . index , crntItem ) ; } } if ( this . props . fValidation ) { this . props . fValidation ( this . props . index , isValid ) ; } } ; /**
     * Delete the item from the collection
     */ private deleteRow = ( ) => { if ( this . props . fDeleteItem ) { this . props . fDeleteItem ( this . props . index ) ; } } ; param field param value private fieldValidation = async ( field , value ) : Promise < string > => { let validation = "" ; if ( field . onGetErrorMessage ) { this . validation [ field . id ] = false ; validation = await field . onGetErrorMessage ( value , this . props . index , this . state . crntItem ) ; } return this . storeFieldValidation ( field . id , validation , true ) ; } ; /**
     * Updates callout and validation state
     */ private async storeFieldValidation ( fieldId , validation , doAllFieldChecks = false ) { this . validation [ fieldId ] = validation === "" ; this . errorCalloutHandler ( fieldId , validation ) ; if ( doAllFieldChecks ) { await this . doAllFieldChecks ( ) ; } return validation ; } /**
     * Custom field validation
     */ private onCustomFieldValidation = async ( fieldId , errorMsg ) => { console . log ( fieldId , errorMsg ) ; if ( fieldId ) { await this . storeFieldValidation ( fieldId , errorMsg , true ) ; } } ; param field param value param item private urlFieldValidation = async ( field , value , item ) : Promise < string > => { let isValid = true ; let validation = "" ; if ( field . onGetErrorMessage ) { validation = await field . onGetErrorMessage ( value , this . props . index , item ) ; isValid = validation === "" ; } else { const regEx = /(http|https)?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/ ; isValid = ( value === null || value . length === 0 || regEx . test ( value ) ) ; validation = isValid ? "" : strings . InvalidUrlError ; } return this . storeFieldValidation ( field . id , validation , true ) ; } ; param field param message private errorCalloutHandler ( fieldId , message ) { this . setState ( ( prevState ) => { let { crntItem , errorMsgs } = prevState ; const fieldIdx = findIndex ( this . props . fields , f => f . id === fieldId ) ; if ( fieldIdx === - 1 ) { return ; } const field = this . props . fields [ fieldIdx ] ; const fieldMsgIdx = findIndex ( errorMsgs , msg => msg . field === field . title ) ; let fieldMsg ; if ( fieldMsgIdx === - 1 ) { fieldMsg = { field : field . title , message : message } ; } else { fieldMsg = errorMsgs [ fieldMsgIdx ] ; if ( fieldMsg ) { fieldMsg . message = message ; } } if ( field . required ) { if ( typeof crntItem [ field . id ] === "undefined" || crntItem [ field . id ] === null || crntItem [ field . id ] === "" ) { fieldMsg . isRequired = true ; } else { fieldMsg . isRequired = false ; } } if ( ! fieldMsg . message && ! fieldMsg . isRequired ) { if ( fieldMsgIdx !== - 1 ) { errorMsgs . splice ( fieldMsgIdx , 1 ) ; } } else { if ( fieldMsgIdx === - 1 ) { errorMsgs . push ( fieldMsg ) ; } } errorMsgs = sortBy ( errorMsgs , [ "field" ] ) ; return { errorMsgs } ; } ) ; } /**
     * Toggle the error callout
     */ private toggleErrorCallout = ( ) => { this . setState ( ( prevState ) => ( { showCallout : ! prevState . showCallout } ) ) ; } ; private hideErrorCallout = ( ) => { this . setState ( { showCallout : false } ) ; } ; param field param item private renderField ( field , item ) { const disableFieldOnEdit = field . disableEdit && ! ! this . props . fUpdateItem ; switch ( field . type ) { case CustomCollectionFieldType . boolean : return < CollectionCheckboxField field = { field } item = { item } disableEdit = { disableFieldOnEdit } fOnValueChange = { this . onValueChanged } fValidation = { this . fieldValidation } / > ; case CustomCollectionFieldType . dropdown : return < CollectionDropdownField field = { field } item = { item } disableEdit = { disableFieldOnEdit } fOnValueChange = { this . onValueChanged } fValidation = { this . fieldValidation } / > ; case CustomCollectionFieldType . number : return < CollectionNumberField field = { field } item = { item } disableEdit = { disableFieldOnEdit } fOnValueChange = { this . onValueChanged } fValidation = { this . fieldValidation } / > ; case CustomCollectionFieldType . fabricIcon : return < CollectionIconField field = { field } item = { item } disableEdit = { disableFieldOnEdit } fOnValueChange = { this . onValueChanged } fValidation = { this . fieldValidation } / > ; case CustomCollectionFieldType . color : return < CollectionColorField field = { field } item = { item } disableEdit = { disableFieldOnEdit } fOnValueChange = { this . onValueChanged } fValidation = { this . fieldValidation } / > ; case CustomCollectionFieldType . url : return < TextField placeholder = { field . placeholder || field . title } value = { item [ field . id ] ? item [ field . id ] : "" } required = { field . required } disabled = { disableFieldOnEdit } className = { styles . collectionDataField } onChange = { ( e , value ) => this . onValueChanged ( field . id , value ) } deferredValidationTime = { field . deferredValidationTime || field . deferredValidationTime >= 0 ? field . deferredValidationTime : 200 } onGetErrorMessage = { async ( value ) => this . urlFieldValidation ( field , value , item ) } inputClassName = "PropertyFieldCollectionData__panel__url-field" / > ; case CustomCollectionFieldType . custom : if ( field . onCustomRender ) { return field . onCustomRender ( field , item [ field . id ] , ( fieldId , value ) => { this . onValueChanged ( fieldId , value ) ; if ( field . onGetErrorMessage ) { this . fieldValidation ( field , value ) ; } } , item , item . uniqueId , this . onCustomFieldValidation ) ; } return null ; case CustomCollectionFieldType . string : default : return < TextField placeholder = { field . placeholder || field . title } className = { styles . collectionDataField } value = { item [ field . id ] ? item [ field . id ] : "" } required = { field . required } disabled = { disableFieldOnEdit } onChange = { ( e , value ) => this . onValueChanged ( field . id , value ) } deferredValidationTime = { field . deferredValidationTime || field . deferredValidationTime >= 0 ? field . deferredValidationTime : 200 } onGetErrorMessage = { async ( value ) => await this . fieldValidation ( field , value ) } inputClassName = "PropertyFieldCollectionData__panel__string-field" / > ; } } private getSortingOptions ( ) { let opts = [ ] ; const { totalItems } = this . props ; for ( let i = 1 ; i <= totalItems ; i ++ ) { opts . push ( { text : i . toString ( ) , key : i } ) ; } return opts ; } private generateEmptyItem ( ) { let emptyItem = { } ; emptyItem . uniqueId = Guid . newGuid ( ) . toString ( ) ; for ( const field of this . props . fields ) { emptyItem [ field . id ] = field . defaultValue || null ; } return emptyItem ; } public render ( ) { const { crntItem , disableAdd } = this . state ; const opts = this . getSortingOptions ( ) ; return ( < div className = { `PropertyFieldCollectionData__panel__table-row ${ styles . tableRow } ${ this . props . index === null ? styles . tableFooter : "" }` } > { ( this . props . sortingEnabled && this . props . totalItems ) && ( < span className = { `PropertyFieldCollectionData__panel__sorting-field ${ styles . tableCell }` } > < Dropdown options = { opts } selectedKey = { this . props . index + 1 } onChanged = { ( opt ) => this . props . fOnSorting ( this . props . index , opt . key as number ) } /> < /span>)} { ( this . props . sortingEnabled && this . props . totalItems === null ) && ( < span className = { `${ styles . tableCell }` } > < /span>)} { this . props . fields . map ( f => ( < span key = { `dataitem-${ f . id }` } className = { `${ styles . tableCell } ${ styles . inputField }` } > { this . renderField ( f , crntItem ) } < /span>))} < span className = { styles . tableCell } > < span ref = { ref => this . calloutCellRef = ref } > < Link title = { strings . CollectionDataItemShowErrorsLabel } className = { styles . errorCalloutLink } disabled = { ! this . state . errorMsgs || this . state . errorMsgs . length === 0 } onClick = { this . toggleErrorCallout } > < Icon iconName = "Error" / > < /Link> < /span> { this . state . showCallout && ( < Callout className = { styles . errorCallout } target = { this . calloutCellRef } isBeakVisible = { true } directionalHint = { DirectionalHint . bottomLeftEdge } directionalHintForRTL = { DirectionalHint . rightBottomEdge } onDismiss = { this . hideErrorCallout } > { ( this . state . errorMsgs && this . state . errorMsgs . length > 0 ) && ( < div className = { styles . errorMsgs } > < p > Field issues : < /p> < ul > { this . state . errorMsgs . map ( ( msg , idx ) => ( < li key = { `${ msg . field }-${ idx }` } > < b > { msg . field } < /b>: {msg.message ? msg.message : msg.isRequired ? strings.CollectionDataItemFieldRequiredLabel : null}</li > ) ) } < /ul> < /div>)} < /Callout>)} < /span> < span className = { styles . tableCell } > { this . props . index !== null ? ( < Link title = { strings . CollectionDeleteRowButtonLabel } disabled = { ! this . props . fDeleteItem || this . props . disableItemDeletion } onClick = { this . deleteRow } > < Icon iconName = "Clear" / > < /Link>) : (<Link title={strings.CollectionAddRowButtonLabel} className={`${disableAdd ? "" : styles.addBtn}`} disabled={disableAdd} onClick={async () => await this.addRow()}> < Icon iconName = "Add" / > < /Link>)} < /span> < /div>); } }