Skip to content

Commit 10095b9

Browse files
author
Tom German
committed
Fixes to desired functionality
1 parent a9577b7 commit 10095b9

File tree

4 files changed

+78
-43
lines changed

4 files changed

+78
-43
lines changed

src/common/utilities/FormulaEvaluation.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class FormulaEvaluation {
3939
[new RegExp(`^(${ValidFuncNames.join('|')})\\(`), "FUNCTION"], // Functions or other words
4040
[/^(true|false)/, "BOOLEAN"], // Boolean literals
4141
[/^\w+/, "WORD"], // Other words, checked against valid variables
42-
[/^&&|^\|\||^==/, "OPERATOR"], // Operators and special characters (match double first)
42+
[/^&&|^\|\||^==|^<>/, "OPERATOR"], // Operators and special characters (match double first)
4343
[/^[+\-*/<>=%!&|?:,()[\]]/, "OPERATOR"], // Operators and special characters
4444
];
4545

@@ -113,6 +113,7 @@ export class FormulaEvaluation {
113113
"<": { precedence: 3, associativity: "left" },
114114
"==": { precedence: 3, associativity: "left" },
115115
"!=": { precedence: 3, associativity: "left" },
116+
"<>": { precedence: 3, associativity: "left" },
116117
">=": { precedence: 3, associativity: "left" },
117118
"<=": { precedence: 3, associativity: "left" },
118119
"&&": { precedence: 2, associativity: "left" },
@@ -256,7 +257,7 @@ export class FormulaEvaluation {
256257
stack.push({ type: operand.type, value: -numericValue });
257258
} else if (token.type === "OPERATOR") {
258259
// Operators have two operands, we pop them from the stack and push an object representing the operation
259-
if (["+", "-", "*", "/", "==", "!=", ">", "<", ">=", "<=", "&&", "||", "%", "&", "|", "?", ":"].includes(token.value as string)) {
260+
if (["+", "-", "*", "/", "==", "!=", "<>", ">", "<", ">=", "<=", "&&", "||", "%", "&", "|", "?", ":"].includes(token.value as string)) {
260261
if (token.value === "?") {
261262
// Ternary operator has three operands, and left and right operators should be top of stack
262263
const colonOperator = stack.pop() as ASTNode;
@@ -338,7 +339,7 @@ export class FormulaEvaluation {
338339
}
339340

340341
// OPERATOR nodes have their OPERANDS evaluated recursively, with the operator applied to the results
341-
if (node.type === "OPERATOR" && ["+", "-", "*", "/", "==", "!=", ">", "<", ">=", "<=", "&&", "||", "%", "&", "|"].includes(node.value as string) && node.operands) {
342+
if (node.type === "OPERATOR" && ["+", "-", "*", "/", "==", "!=", "<>", ">", "<", ">=", "<=", "&&", "||", "%", "&", "|"].includes(node.value as string) && node.operands) {
342343

343344
const leftValue = this.evaluateASTNode(node.operands[0], context);
344345
const rightValue = this.evaluateASTNode(node.operands[1], context);
@@ -361,6 +362,7 @@ export class FormulaEvaluation {
361362
case "/": return leftValue / rightValue;
362363
case "==": return leftValue === rightValue ? 1 : 0;
363364
case "!=": return leftValue !== rightValue ? 1 : 0;
365+
case "<>": return leftValue !== rightValue ? 1 : 0;
364366
case ">": return leftValue > rightValue ? 1 : 0;
365367
case "<": return leftValue < rightValue ? 1 : 0;
366368
case ">=": return leftValue >= rightValue ? 1 : 0;

src/controls/dynamicForm/DynamicForm.tsx

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,24 @@ export class DynamicForm extends React.Component<
113113

114114
public componentDidUpdate(prevProps: IDynamicFormProps, prevState: IDynamicFormState): void {
115115
if (!isEqual(prevProps, this.props)) {
116+
// Props have changed due to parent component or workbench config, reset state
116117
this.setState({
117118
infoErrorMessages: [], // Reset info/error messages
118119
validationErrors: {} // Reset validation errors
119120
}, () => {
120-
this.getListInformation()
121-
.then(() => {
122-
/* no-op; */
123-
})
124-
.catch((err) => {
125-
/* no-op; */
126-
console.error(err);
127-
});
121+
// If listId or listItemId have changed, reload list information
122+
if (prevProps.listId !== this.props.listId || prevProps.listItemId !== this.props.listItemId) {
123+
this.getListInformation()
124+
.then(() => {
125+
/* no-op; */
126+
})
127+
.catch((err) => {
128+
/* no-op; */
129+
console.error(err);
130+
});
131+
} else {
132+
this.performValidation();
133+
}
128134
});
129135
}
130136
}
@@ -327,7 +333,7 @@ export class DynamicForm extends React.Component<
327333
}
328334

329335
// Check min and max values for number fields
330-
if (field.fieldType === "Number") {
336+
if (field.fieldType === "Number" && field.newValue !== undefined) {
331337
if ((field.newValue < field.minimumValue) || (field.newValue > field.maximumValue)) {
332338
shouldBeReturnBack = true;
333339
}
@@ -336,9 +342,13 @@ export class DynamicForm extends React.Component<
336342
});
337343

338344
// Perform validation
339-
const validationErrors = await this.evaluateFormulas(this.state.validationFormulas, true) as Record<string, string>;
340-
if (Object.keys(validationErrors).length > 0) {
341-
shouldBeReturnBack = true;
345+
const validationDisabled = this.props.useFieldValidation === false;
346+
let validationErrors: Record<string, string> = {};
347+
if (!validationDisabled) {
348+
validationErrors = await this.evaluateFormulas(this.state.validationFormulas, true, true, this.state.hiddenByFormula) as Record<string, string>;
349+
if (Object.keys(validationErrors).length > 0) {
350+
shouldBeReturnBack = true;
351+
}
342352
}
343353

344354
// If validation failed, return without saving
@@ -462,6 +472,8 @@ export class DynamicForm extends React.Component<
462472
}
463473
}
464474

475+
let apiError: string;
476+
465477
// If we have the item ID, we simply need to update it
466478
let newETag: string | undefined = undefined;
467479
if (listItemId) {
@@ -480,6 +492,7 @@ export class DynamicForm extends React.Component<
480492
);
481493
}
482494
} catch (error) {
495+
apiError = error.message;
483496
if (onSubmitError) {
484497
onSubmitError(objects, error);
485498
}
@@ -505,6 +518,7 @@ export class DynamicForm extends React.Component<
505518
);
506519
}
507520
} catch (error) {
521+
apiError = error.message;
508522
if (onSubmitError) {
509523
onSubmitError(objects, error);
510524
}
@@ -551,6 +565,7 @@ export class DynamicForm extends React.Component<
551565
);
552566
}
553567
} catch (error) {
568+
apiError = error.message;
554569
if (onSubmitError) {
555570
onSubmitError(objects, error);
556571
}
@@ -561,6 +576,7 @@ export class DynamicForm extends React.Component<
561576
this.setState({
562577
isSaving: false,
563578
etag: newETag,
579+
infoErrorMessages: apiError ? [{ type: MessageBarType.error, message: apiError }] : [],
564580
});
565581
} catch (error) {
566582
if (onSubmitError) {
@@ -577,7 +593,8 @@ export class DynamicForm extends React.Component<
577593
internalName: string,
578594
// eslint-disable-next-line @typescript-eslint/no-explicit-any
579595
newValue: any,
580-
additionalData?: FieldChangeAdditionalData
596+
validate: boolean,
597+
additionalData?: FieldChangeAdditionalData,
581598
): Promise<void> => {
582599

583600
const fieldCol = cloneDeep(this.state.fieldCollection || []);
@@ -652,20 +669,33 @@ export class DynamicForm extends React.Component<
652669
field.stringValue = emails.join(";");
653670
}
654671

672+
const validationErrors = {...this.state.validationErrors};
673+
if (validationErrors[field.columnInternalName]) delete validationErrors[field.columnInternalName];
674+
655675
this.setState({
656676
fieldCollection: fieldCol,
657-
}, this.performValidation);
677+
validationErrors
678+
}, () => {
679+
if (validate) this.performValidation();
680+
});
658681
};
659682

660683
/** Validation callback, used when form first loads (getListInformation) and following onChange */
661684
private performValidation = (skipFieldValueValidation?: boolean): void => {
662685
const { useClientSideValidation, useFieldValidation } = this.props;
663-
const clientSideValidationDisabled = useClientSideValidation === false;
664-
const fieldValidationDisabled = useFieldValidation === false;
665-
const hiddenByFormula: string[] = !clientSideValidationDisabled ? this.evaluateColumnVisibilityFormulas() : [];
666-
let validationErrors = { ...this.state.validationErrors };
667-
if (!skipFieldValueValidation && !fieldValidationDisabled) validationErrors = this.evaluateFieldValueFormulas();
668-
this.setState({ hiddenByFormula, validationErrors });
686+
const { clientValidationFormulas, validationFormulas } = this.state;
687+
if (Object.keys(clientValidationFormulas).length || Object.keys(validationFormulas).length) {
688+
this.setState({
689+
isSaving: true, // Disable save btn and fields while validation in progress
690+
}, () => {
691+
const clientSideValidationDisabled = useClientSideValidation === false;
692+
const fieldValidationDisabled = useFieldValidation === false;
693+
const hiddenByFormula: string[] = !clientSideValidationDisabled ? this.evaluateColumnVisibilityFormulas() : [];
694+
let validationErrors = { ...this.state.validationErrors };
695+
if (!skipFieldValueValidation && !fieldValidationDisabled) validationErrors = this.evaluateFieldValueFormulas(hiddenByFormula);
696+
this.setState({ hiddenByFormula, isSaving: false, validationErrors });
697+
});
698+
}
669699
}
670700

671701
/** Determines visibility of fields that have show/hide formulas set in Edit Form > Edit Columns > Edit Conditional Formula */
@@ -674,8 +704,8 @@ export class DynamicForm extends React.Component<
674704
}
675705

676706
/** Evaluates field validation formulas set in column settings and returns a Record of error messages */
677-
private evaluateFieldValueFormulas = (): Record<string, string> => {
678-
return this.evaluateFormulas(this.state.validationFormulas, true, true) as Record<string, string>;
707+
private evaluateFieldValueFormulas = (hiddenFields: string[]): Record<string, string> => {
708+
return this.evaluateFormulas(this.state.validationFormulas, true, true, hiddenFields) as Record<string, string>;
679709
}
680710

681711
/**
@@ -688,7 +718,8 @@ export class DynamicForm extends React.Component<
688718
private evaluateFormulas = (
689719
formulas: Record<string, Pick<ISPField, "ValidationFormula" | "ValidationMessage">>,
690720
returnMessages = true,
691-
requireValue: boolean = false
721+
requireValue: boolean = false,
722+
ignoreFields: string[] = []
692723
): string[] | Record<string, string> => {
693724
const { fieldCollection } = this.state;
694725
const results: Record<string, string> = {};
@@ -697,6 +728,7 @@ export class DynamicForm extends React.Component<
697728
if (formulas[fieldName]) {
698729
const field = fieldCollection.find(f => f.columnInternalName === fieldName);
699730
if (!field) continue;
731+
if (ignoreFields.indexOf(fieldName) > -1) continue; // Skip fields that are being ignored (e.g. hidden by formula)
700732
const formula = formulas[fieldName].ValidationFormula;
701733
const message = formulas[fieldName].ValidationMessage;
702734
if (!formula) continue;

src/controls/dynamicForm/dynamicField/DynamicField.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
176176
{...dropdownOptions}
177177
defaultSelectedKey={valueToDisplay ? undefined : defaultValue}
178178
selectedKey={typeof valueToDisplay === "object" ? valueToDisplay?.key : valueToDisplay}
179-
onChange={(e, option) => { this.onChange(option); }}
179+
onChange={(e, option) => { this.onChange(option, true); }}
180180
onBlur={this.onBlur}
181181
errorMessage={errorText} />
182182
{descriptionEl}
@@ -209,7 +209,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
209209
context={context}
210210
disabled={disabled}
211211
placeholder={placeholder}
212-
onChange={(newValue) => { this.onChange(newValue); }}
212+
onChange={(newValue) => { this.onChange(newValue, true); }}
213213
defaultValue={valueToDisplay !== undefined ? valueToDisplay : defaultValue}
214214
errorMessage={errorText}
215215
/>
@@ -233,7 +233,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
233233
enableDefaultSuggestions={true}
234234
keyColumnInternalName='Id'
235235
itemLimit={1}
236-
onSelectedItem={(newValue) => { this.onChange(newValue); }}
236+
onSelectedItem={(newValue) => { this.onChange(newValue, true); }}
237237
context={context}
238238
/>
239239
{descriptionEl}
@@ -257,7 +257,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
257257
enableDefaultSuggestions={true}
258258
keyColumnInternalName='Id'
259259
itemLimit={100}
260-
onSelectedItem={(newValue) => { this.onChange(newValue); }}
260+
onSelectedItem={(newValue) => { this.onChange(newValue, true); }}
261261
context={context}
262262
/>
263263
{descriptionEl}
@@ -318,7 +318,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
318318
className={styles.pickersContainer}
319319
formatDate={(date) => { return date.toLocaleDateString(context.pageContext.cultureInfo.currentCultureName); }}
320320
value={valueToDisplay !== undefined ? valueToDisplay : defaultValue}
321-
onSelectDate={(newDate) => { this.onChange(newDate); }}
321+
onSelectDate={(newDate) => { this.onChange(newDate, true); }}
322322
disabled={disabled}
323323
firstDayOfWeek={firstDayOfWeek}
324324
/>}
@@ -329,7 +329,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
329329
placeholder={placeholder}
330330
formatDate={(date) => { return date.toLocaleDateString(context.pageContext.cultureInfo.currentCultureName); }}
331331
value={valueToDisplay !== undefined ? valueToDisplay : defaultValue}
332-
onChange={(newDate) => { this.onChange(newDate); }}
332+
onChange={(newDate) => { this.onChange(newDate, true); }}
333333
disabled={disabled}
334334
firstDayOfWeek={firstDayOfWeek}
335335
/>
@@ -350,7 +350,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
350350
checked={valueToDisplay}
351351
onText={strings.Yes}
352352
offText={strings.No}
353-
onChange={(e, checkedvalue) => { this.onChange(checkedvalue); }}
353+
onChange={(e, checkedvalue) => { this.onChange(checkedvalue, true); }}
354354
disabled={disabled}
355355
/>
356356
{descriptionEl}
@@ -374,7 +374,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
374374
showHiddenInUI={false}
375375
principalTypes={principalType === 'PeopleOnly' ? [PrincipalType.User] : [PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.DistributionList, PrincipalType.SecurityGroup]}
376376
resolveDelay={1000}
377-
onChange={(items) => { this.onChange(items); }}
377+
onChange={(items) => { this.onChange(items, true); }}
378378
disabled={disabled}
379379
/>
380380
{descriptionEl}
@@ -398,7 +398,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
398398
showHiddenInUI={false}
399399
principalTypes={principalType === 'PeopleOnly' ? [PrincipalType.User] : [PrincipalType.User, PrincipalType.SharePointGroup, PrincipalType.DistributionList, PrincipalType.SecurityGroup]}
400400
resolveDelay={1000}
401-
onChange={(items) => { this.onChange(items); }}
401+
onChange={(items) => { this.onChange(items, true); }}
402402
disabled={disabled}
403403
/>
404404
{descriptionEl}
@@ -498,7 +498,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
498498
anchorId={fieldAnchorId}
499499
panelTitle={strings.DynamicFormTermPanelTitle}
500500
context={context}
501-
onChange={(newValue?: IPickerTerms) => { this.onChange(newValue); }}
501+
onChange={(newValue?: IPickerTerms) => { this.onChange(newValue, true); }}
502502
isTermSetSelectable={false}
503503
/>
504504
</div>
@@ -523,7 +523,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
523523
anchorId={fieldAnchorId}
524524
panelTitle={strings.DynamicFormTermPanelTitle}
525525
context={context}
526-
onChange={(newValue?: IPickerTerms) => { this.onChange(newValue); }}
526+
onChange={(newValue?: IPickerTerms) => { this.onChange(newValue, true); }}
527527
isTermSetSelectable={false} />
528528
</div>
529529
{descriptionEl}
@@ -577,18 +577,18 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
577577
});
578578

579579
if (onChanged) {
580-
onChanged(columnInternalName, currValue);
580+
onChanged(columnInternalName, currValue, false);
581581
}
582582
}
583583

584-
private onChange = (value: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any
584+
private onChange = (value: any, callValidation = false): void => { // eslint-disable-line @typescript-eslint/no-explicit-any
585585
const {
586586
onChanged,
587587
columnInternalName
588588
} = this.props;
589589

590590
if (onChanged) {
591-
onChanged(columnInternalName, value);
591+
onChanged(columnInternalName, value, callValidation);
592592
}
593593
this.setState({
594594
changedValue: value
@@ -599,6 +599,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
599599
if (this.state.changedValue === null && this.props.defaultValue === "") {
600600
this.setState({ changedValue: "" });
601601
}
602+
this.props.onChanged(this.props.columnInternalName, this.state.changedValue, true);
602603
}
603604

604605
private getRequiredErrorText = (): string => {
@@ -683,7 +684,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
683684
}
684685

685686
this.setState({ changedValue: selectedItemArr });
686-
this.props.onChanged(this.props.columnInternalName, selectedItemArr);
687+
this.props.onChanged(this.props.columnInternalName, selectedItemArr, true);
687688
} catch (error) {
688689
console.log(`Error MultiChoice_selection`, error);
689690
}
@@ -713,7 +714,7 @@ export class DynamicField extends React.Component<IDynamicFieldProps, IDynamicFi
713714
changedValue: newValue
714715
});
715716
if (onChanged) {
716-
onChanged(columnInternalName, newValue, file);
717+
onChanged(columnInternalName, newValue, true, file);
717718
}
718719
}
719720
catch (error) {

src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface IDynamicFieldProps {
3636
value?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
3737

3838
/** Fired by DynamicField when a field value is changed */
39-
onChanged?: (columnInternalName: string, newValue: any, additionalData?: FieldChangeAdditionalData) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
39+
onChanged?: (columnInternalName: string, newValue: any, validate: boolean, additionalData?: FieldChangeAdditionalData) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
4040

4141
/** Represents the value of the field as updated by the user. Only updated by fields when changed. */
4242
newValue?: any; // eslint-disable-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)