1
1
/* eslint-disable @microsoft/spfx/no-async-await */
2
2
import { SPHttpClient } from "@microsoft/sp-http" ;
3
- import { IRenderListDataAsStreamResult , sp } from "@pnp/sp/presets/all" ;
4
3
import * as strings from "ControlStrings" ;
5
4
import {
6
5
DefaultButton ,
@@ -11,6 +10,7 @@ import { ProgressIndicator } from "office-ui-fabric-react/lib/ProgressIndicator"
11
10
import { IStackTokens , Stack } from "office-ui-fabric-react/lib/Stack" ;
12
11
import * as React from "react" ;
13
12
import { ISPField , IUploadImageResult } from "../../common/SPEntities" ;
13
+ import { FormulaEvaluation } from "../../common/utilities/FormulaEvaluation" ;
14
14
import SPservice from "../../services/SPService" ;
15
15
import { IFilePickerResult } from "../filePicker" ;
16
16
import { DynamicField } from "./dynamicField" ;
@@ -32,6 +32,7 @@ import "@pnp/sp/lists";
32
32
import "@pnp/sp/content-types" ;
33
33
import "@pnp/sp/folders" ;
34
34
import "@pnp/sp/items" ;
35
+ import { sp } from "@pnp/sp" ;
35
36
36
37
const stackTokens : IStackTokens = { childrenGap : 20 } ;
37
38
@@ -43,6 +44,7 @@ export class DynamicForm extends React.Component<
43
44
IDynamicFormState
44
45
> {
45
46
private _spService : SPservice ;
47
+ private _formulaEvaluation : FormulaEvaluation ;
46
48
private webURL = this . props . webAbsoluteUrl
47
49
? this . props . webAbsoluteUrl
48
50
: this . props . context . pageContext . web . absoluteUrl ;
@@ -71,12 +73,16 @@ export class DynamicForm extends React.Component<
71
73
fieldCollection : [ ] ,
72
74
validationFormulas : { } ,
73
75
clientValidationFormulas : { } ,
76
+ validationErrors : { } ,
77
+ hiddenByFormula : [ ] ,
74
78
isValidationErrorDialogOpen : false ,
75
79
} ;
76
80
// Get SPService Factory
77
81
this . _spService = this . props . webAbsoluteUrl
78
82
? new SPservice ( this . props . context , this . props . webAbsoluteUrl )
79
83
: new SPservice ( this . props . context ) ;
84
+ // Formula Validation util
85
+ this . _formulaEvaluation = new FormulaEvaluation ( this . props . context ) ;
80
86
}
81
87
82
88
/**
@@ -96,7 +102,7 @@ export class DynamicForm extends React.Component<
96
102
* Default React component render method
97
103
*/
98
104
public render ( ) : JSX . Element {
99
- const { fieldCollection, isSaving } = this . state ;
105
+ const { fieldCollection, hiddenByFormula , isSaving, validationErrors } = this . state ;
100
106
101
107
const fieldOverrides = this . props . fieldOverrides ;
102
108
@@ -112,6 +118,13 @@ export class DynamicForm extends React.Component<
112
118
) : (
113
119
< div >
114
120
{ fieldCollection . map ( ( v , i ) => {
121
+ if ( hiddenByFormula . find ( h => h === v . columnInternalName ) ) {
122
+ return null ;
123
+ }
124
+ let validationErrorMessage : string = "" ;
125
+ if ( validationErrors [ v . columnInternalName ] ) {
126
+ validationErrorMessage = validationErrors [ v . columnInternalName ] ;
127
+ }
115
128
if (
116
129
fieldOverrides &&
117
130
Object . prototype . hasOwnProperty . call (
@@ -127,6 +140,7 @@ export class DynamicForm extends React.Component<
127
140
key = { v . columnInternalName }
128
141
{ ...v }
129
142
disabled = { v . disabled || isSaving }
143
+ validationErrorMessage = { validationErrorMessage }
130
144
/>
131
145
) ;
132
146
} ) }
@@ -215,12 +229,17 @@ export class DynamicForm extends React.Component<
215
229
}
216
230
}
217
231
} ) ;
232
+ const validationErrors = await this . evaluateFormulas ( this . state . validationFormulas , true ) as Record < string , string > ;
233
+ if ( Object . keys ( validationErrors ) . length > 0 ) {
234
+ shouldBeReturnBack = true ;
235
+ }
218
236
if ( shouldBeReturnBack ) {
219
237
this . setState ( {
220
238
fieldCollection : fields ,
221
239
isValidationErrorDialogOpen :
222
240
this . props . validationErrorDialogProps
223
241
?. showDialogOnValidationError === true ,
242
+ validationErrors
224
243
} ) ;
225
244
return ;
226
245
}
@@ -416,11 +435,10 @@ export class DynamicForm extends React.Component<
416
435
// trigger when the user change any value in the form
417
436
private onChange = async (
418
437
internalName : string ,
438
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
419
439
newValue : any ,
420
440
additionalData ?: FieldChangeAdditionalData
421
441
) : Promise < void > => {
422
- // eslint-disable-line @typescript-eslint/no-explicit-any
423
- // try {
424
442
const fieldCol = ( this . state . fieldCollection || [ ] ) . slice ( ) ;
425
443
const field = fieldCol . filter ( ( element , i ) => {
426
444
return element . columnInternalName === internalName ;
@@ -462,9 +480,85 @@ export class DynamicForm extends React.Component<
462
480
}
463
481
this . setState ( {
464
482
fieldCollection : fieldCol ,
465
- } ) ;
483
+ } , this . performValidation ) ;
466
484
} ;
467
485
486
+ /** Validation callback, used when form first loads (getListInformation) and following onChange */
487
+ private performValidation = async ( skipFieldValueValidation ?: boolean ) : Promise < void > => {
488
+ const hiddenByFormula = await this . evaluateColumnVisibilityFormulas ( ) ;
489
+ let validationErrors = { ...this . state . validationErrors } ;
490
+ if ( ! skipFieldValueValidation ) validationErrors = await this . evaluateFieldValueFormulas ( ) ;
491
+ this . setState ( { hiddenByFormula, validationErrors } ) ;
492
+ }
493
+
494
+ /** Determines visibility of fields that have show/hide formulas set in Edit Form > Edit Columns > Edit Conditional Formula */
495
+ private evaluateColumnVisibilityFormulas = async ( ) : Promise < string [ ] > => {
496
+ return await this . evaluateFormulas ( this . state . clientValidationFormulas , false ) as string [ ] ;
497
+ }
498
+
499
+ /** Evaluates field validation formulas set in column settings and returns a Record of error messages */
500
+ private evaluateFieldValueFormulas = async ( ) : Promise < Record < string , string > > => {
501
+ return await this . evaluateFormulas ( this . state . validationFormulas , true , true ) as Record < string , string > ;
502
+ }
503
+
504
+ /**
505
+ * Evaluates formulas and returns a Record of error messages or an array of column names that have failed validation
506
+ * @param formulas A Record / dictionary-like object, where key is internal column name and value is an object with ValidationFormula and ValidationMessage properties
507
+ * @param returnMessages Determines whether a Record of error messages is returned or an array of column names that have failed validation
508
+ * @param requireValue Set to true if the formula should only be evaluated when the field has a value
509
+ * @returns
510
+ */
511
+ private evaluateFormulas = async (
512
+ formulas : Record < string , Pick < ISPField , "ValidationFormula" | "ValidationMessage" > > ,
513
+ returnMessages = true ,
514
+ requireValue : boolean = false
515
+ ) : Promise < string [ ] | Record < string , string > > => {
516
+ const { fieldCollection } = this . state ;
517
+ const results : Record < string , string > = { } ;
518
+ for ( let i = 0 ; i < Object . keys ( formulas ) . length ; i ++ ) {
519
+ const fieldName = Object . keys ( formulas ) [ i ] ;
520
+ if ( formulas [ fieldName ] ) {
521
+ const field = fieldCollection . find ( f => f . columnInternalName === fieldName ) ;
522
+ if ( ! field ) continue ;
523
+ const formula = formulas [ fieldName ] . ValidationFormula ;
524
+ const message = formulas [ fieldName ] . ValidationMessage ;
525
+ if ( ! formula ) continue ;
526
+ const context = this . getFormValues ( ) ;
527
+ if ( requireValue && ! context [ fieldName ] ) continue ;
528
+ const result = await this . _formulaEvaluation . evaluate ( formula , context ) ;
529
+ if ( Boolean ( result ) !== true ) {
530
+ results [ fieldName ] = message ;
531
+ }
532
+ }
533
+ }
534
+ if ( ! returnMessages ) { return Object . keys ( results ) ; }
535
+ return results ;
536
+ }
537
+
538
+ private getFormValues = ( ) : Record < string , unknown > => {
539
+ const { fieldCollection } = this . state ;
540
+ return fieldCollection . reduce ( ( prev , cur ) => {
541
+ let value : unknown ;
542
+ switch ( cur . fieldType ) {
543
+ case "Lookup" :
544
+ case "Choice" :
545
+ case "TaxonomyFieldType" :
546
+ value = cur . newValue ?. key ;
547
+ break ;
548
+ case "LookupMulti" :
549
+ case "MultiChoice" :
550
+ case "TaxonomyFieldTypeMulti" :
551
+ value = cur . newValue ?. map ( ( v ) => v . key ) ;
552
+ break ;
553
+ default :
554
+ value = cur . newValue ;
555
+ break ;
556
+ }
557
+ prev [ cur . columnInternalName ] = value ;
558
+ return prev ;
559
+ } , { } as Record < string , unknown > ) ;
560
+ }
561
+
468
562
private getListInformation = async ( ) : Promise < void > => {
469
563
const {
470
564
listId,
@@ -718,17 +812,13 @@ export class DynamicForm extends React.Component<
718
812
tempFields . sort ( ( a , b ) => a . Order - b . Order ) ;
719
813
}
720
814
}
721
-
722
- // Do formatting and validation parsing here
723
- console . log ( 'Validation Formulas' , validationFormulas ) ;
724
- console . log ( 'Client Side Validation Formulas' , clientValidationFormulas ) ;
725
815
726
816
this . setState ( {
727
817
clientValidationFormulas,
728
818
fieldCollection : tempFields ,
729
819
validationFormulas,
730
820
etag
731
- } ) ;
821
+ } , ( ) => this . performValidation ( true ) ) ;
732
822
733
823
} catch ( error ) {
734
824
console . log ( `Error get field informations` , error ) ;
@@ -1047,8 +1137,8 @@ export class DynamicForm extends React.Component<
1047
1137
listId : string ,
1048
1138
contentTypeId : string | undefined ,
1049
1139
webUrl ?: string
1050
- ) : Promise < any > => {
1051
- // eslint-disable-line @typescript-eslint/no-explicit- any
1140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1141
+ ) : Promise < any > => {
1052
1142
try {
1053
1143
const { context } = this . props ;
1054
1144
const webAbsoluteUrl = ! webUrl ? this . webURL : webUrl ;
0 commit comments