Skip to content

Commit

Permalink
Merge pull request #291 from nhuyvan/form-validation-improvement
Browse files Browse the repository at this point in the history
- Add a new validator to check for range membership
  • Loading branch information
babybeet authored and Nhuy committed Feb 20, 2020
2 parents 68e5e0b + 658a2a3 commit bf27e4e
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 27 deletions.
6 changes: 6 additions & 0 deletions frontend/src/app/shared/form/textarea/TextArea.common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
.CodeMirror-gutters {
z-index: 0;
}

.errored-line {
display: inline-block;
text-decoration-style: wavy;
text-decoration-line: underline;
}
}

.textarea__input-box-container {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/shared/form/textarea/TextArea.dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
.CodeMirror-linenumber {
color: #fff;
}

.errored-line {
text-decoration-color: #f00;
}
}

.textarea__input-box {
Expand Down Expand Up @@ -95,4 +99,8 @@
.CodeMirror-linenumber {
color: #6c6c6c;
}

.errored-line {
text-decoration-color: #6c6c6c;
}
}
8 changes: 8 additions & 0 deletions frontend/src/app/shared/form/textarea/TextArea.light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
}
}
}

.errored-line {
text-decoration-color: #f00;
}
}

.textarea__input-box,
Expand Down Expand Up @@ -67,4 +71,8 @@
color: #dfdfdf;
}

.errored-line {
text-decoration-color: #dfdfdf;
}

}
36 changes: 33 additions & 3 deletions frontend/src/app/shared/form/textarea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ export class TextArea extends React.Component<Props, State> {
}
});

this.props.formControlModel.validityChanges()
.subscribe({
next: isValid => {
if (this._codeMirror) {
if (!isValid) {
const currentCursor = this._codeMirror.getCursor();
const startCursor = {
line: currentCursor.line,
ch: 0
};
this._codeMirror.markText(startCursor, currentCursor).clear();
this._codeMirror.markText(startCursor, currentCursor, { className: 'errored-line' });
} else {
for (const mark of this._codeMirror.getAllMarks()) {
mark.clear();
}
}
}
}
});

this._valueChanges.pipe(debounceTime(250))
.subscribe({
next: value => {
Expand Down Expand Up @@ -105,6 +126,13 @@ export class TextArea extends React.Component<Props, State> {
mode: {
name: 'javascript',
json: true
},
indentUnit: 4
});
this._codeMirror.setOption('extraKeys', {
Tab: codeMirror => {
const spaces = ' '.repeat(codeMirror.getOption('indentUnit'));
codeMirror.replaceSelection(spaces);
}
});
this._codeMirror.on('change', this._onValueChange);
Expand Down Expand Up @@ -150,9 +178,11 @@ export class TextArea extends React.Component<Props, State> {
this.props.formControlModel.cleanup();
window.removeEventListener('mousemove', this.resize, false);
window.removeEventListener('mouseup', this.stopResize, false);
this._codeMirror.off('change', this._onValueChange);
this._codeMirror.off('focus', this._onCodeMirrorEditorFocused);
this._codeMirror.off('blur', this._onCodeMirrorEditorFocusLost);
if (this._codeMirror) {
this._codeMirror.off('change', this._onValueChange);
this._codeMirror.off('focus', this._onCodeMirrorEditorFocused);
this._codeMirror.off('blur', this._onCodeMirrorEditorFocusLost);
}
}

render() {
Expand Down
29 changes: 20 additions & 9 deletions frontend/src/app/shared/form/validation/Validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ export class Validators {
};
}

static checkValidJSON(): Validator {
return control => {
static checkValidJSON(subjectDisplayName: string): Validator {
return (control: FormControlModel<string>) => {
const value = control.getValue();
try {
if (control.getValue() !== '') {
JSON.parse(control.getValue());
if (value !== '') {
JSON.parse(value);
}
return {
isValid: true,
errorMessage: ''
};
} catch (e) {
} catch {
return {
errorMessage: `Invalid JSON: ${e.message.replace('JSON.parse: ', '')}`,
errorMessage: `${subjectDisplayName} is not a valid JSON`,
isValid: false
};
}
Expand All @@ -50,7 +51,7 @@ export class Validators {
}

private static _checkPattern(errorMessage: string, pattern: RegExp): Validator {
return control => ({
return (control: FormControlModel<number> | FormControlModel<string>) => ({
isValid: pattern.test(String(control.getValue())),
errorMessage
});
Expand All @@ -63,15 +64,25 @@ export class Validators {
});
}

static checkBetween(subjectDisplayName: string, min: number, max: number): Validator {
return (control: FormControlModel<number>) => {
const value = control.getValue();
return {
isValid: min <= value && value <= max,
errorMessage: `${subjectDisplayName} must be between ${min} and ${max}`
};
};
}

static checkMin(subjectDisplayName: string, min: number): Validator {
return control => ({
return (control: FormControlModel<number>) => ({
isValid: control.getValue() >= min,
errorMessage: `${subjectDisplayName} must be greater than or equal to ${min}`
});
}

static checkMax(subjectDisplayName: string, max: number): Validator {
return control => ({
return (control: FormControlModel<number>) => ({
isValid: control.getValue() <= max,
errorMessage: `${subjectDisplayName} must be less than or equal to ${max}`
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ApplicationConfigurationTab extends React.Component<Props, State> {
readonly nameFormControlModel = new FormControlModel('');
readonly configStringFormControlModel = new FormControlModel(
'',
[Validators.checkValidJSON()]
[Validators.checkValidJSON('Application configuration')]
);

constructor(props: Props) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,25 @@ export class ServiceConfiguration extends React.Component<Props, State> {
}

private _resolveValidatorsForUserInputSpec(label: string, userInputSpec: ServiceConfigUserInputSpec) {
const validators = [] as Validator[];
const validators = [Validators.checkNotEmpty(label)] as Validator[];
switch (userInputSpec.type) {
case 'float':
case 'int':
validators.push(
Validators.checkNotEmpty(label),
Validators.checkValidNumber(label)
);
validators.push(Validators.checkValidNumber(label));
const min = userInputSpec.min_value;
const max = userInputSpec.max_value;
if (typeof min === 'number') {
validators.push(Validators.checkMin(label, min));
}
if (typeof max === 'number') {
if (typeof max === 'number') {
validators.push(Validators.checkBetween(label, min, max));
} else {
validators.push(Validators.checkMin(label, min));
}
} else if (typeof max === 'number') {
validators.push(Validators.checkMax(label, max));
}
break;
case 'object':
validators.push(
Validators.checkNotEmpty(label),
Validators.checkValidJSON()
);
validators.push(Validators.checkValidJSON(label));
break;
}
return validators;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class SimulationConfigurationTab extends React.Component<Props, State> {
JSON.stringify(this.props.simulationConfig.model_creation_config, null, 4),
[
Validators.checkNotEmpty('Model creation config'),
Validators.checkValidJSON()
Validators.checkValidJSON('Model creation configuration')
]
);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/stomp-client/StompClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class StompClient extends React.Component<Props, State> {
'{}',
[
Validators.checkNotEmpty('Request Body'),
Validators.checkValidJSON()
Validators.checkValidJSON('Request body')
]
)
});
Expand Down

0 comments on commit bf27e4e

Please sign in to comment.