mirror of https://github.com/Squidex/squidex.git
Browse Source
* Local errors and rx fixes. * Performance and recursive fixes. * User experience fixed. * Errors fixed. * Build fix. * Try to disable watch. * Watchers test * Remove stuff.pull/596/head
committed by
GitHub
37 changed files with 980 additions and 374 deletions
@ -0,0 +1,184 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { FormArray, FormControl, FormGroup } from '@angular/forms'; |
|||
import { ErrorDto } from '@app/shared'; |
|||
import { ErrorValidator } from './error-validator'; |
|||
|
|||
describe('ErrorValidator', () => { |
|||
const validator = new ErrorValidator(); |
|||
|
|||
const control = new FormGroup({ |
|||
nested1: new FormArray([ |
|||
new FormGroup({ |
|||
nested2: new FormControl() |
|||
}) |
|||
]) |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
control.reset([]); |
|||
}); |
|||
|
|||
it('should return no message when error is null', () => { |
|||
validator.setError(null); |
|||
|
|||
const error = validator.validator(control); |
|||
|
|||
expect(error).toBeNull(); |
|||
}); |
|||
|
|||
it('should return no message when error does not match', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1Property: My Error.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1')!); |
|||
|
|||
expect(error).toBeNull(); |
|||
}); |
|||
|
|||
it('should return matching error', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'other, nested1: My Error.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1')!); |
|||
|
|||
expect(error).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
it('should return matching error twice if value does not change', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1: My Error.' |
|||
])); |
|||
|
|||
const error1 = validator.validator(control.get('nested1')!); |
|||
const error2 = validator.validator(control.get('nested1')!); |
|||
|
|||
expect(error1).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
|
|||
expect(error2).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
it('should not return matching error again if value has changed', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1[1].nested2: My Error.' |
|||
])); |
|||
|
|||
const nested = control.get('nested1.0.nested2'); |
|||
|
|||
nested?.setValue('a'); |
|||
const error1 = validator.validator(nested!); |
|||
|
|||
nested?.setValue('b'); |
|||
const error2 = validator.validator(nested!); |
|||
|
|||
expect(error1).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
|
|||
expect(error2).toBeNull(); |
|||
}); |
|||
|
|||
it('should not return matching error again if value has changed to initial', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1[1].nested2: My Error.' |
|||
])); |
|||
|
|||
const nested = control.get('nested1.0.nested2'); |
|||
|
|||
nested?.setValue('a'); |
|||
const error1 = validator.validator(nested!); |
|||
|
|||
nested?.setValue('b'); |
|||
const error2 = validator.validator(nested!); |
|||
|
|||
nested?.setValue('a'); |
|||
const error3 = validator.validator(nested!); |
|||
|
|||
expect(error1).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
|
|||
expect(error2).toBeNull(); |
|||
expect(error3).toBeNull(); |
|||
}); |
|||
|
|||
it('should return matching errors', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1: My Error1.', |
|||
'nested1: My Error2.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1')!); |
|||
|
|||
expect(error).toEqual({ |
|||
custom: { |
|||
errors: ['My Error1.', 'My Error2.'] |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
it('should return deeply matching error', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1[1].nested2: My Error.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1.0.nested2')!); |
|||
|
|||
expect(error).toEqual({ |
|||
custom: { |
|||
errors: ['My Error.'] |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
it('should return partial matching error', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1[1].nested2: My Error.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1.0')!); |
|||
|
|||
expect(error).toEqual({ |
|||
custom: { |
|||
errors: ['nested2: My Error.'] |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
it('should return partial matching index error', () => { |
|||
validator.setError(new ErrorDto(500, 'Error', [ |
|||
'nested1[1].nested2: My Error.' |
|||
])); |
|||
|
|||
const error = validator.validator(control.get('nested1')!); |
|||
|
|||
expect(error).toEqual({ |
|||
custom: { |
|||
errors: ['[1].nested2: My Error.'] |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,76 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ValidatorFn } from '@angular/forms'; |
|||
import { ErrorDto } from '@app/framework/internal'; |
|||
import { getControlPath } from './forms-helper'; |
|||
|
|||
export class ErrorValidator { |
|||
private values: { [path: string]: { value: any } } = {}; |
|||
private error: ErrorDto | undefined | null; |
|||
|
|||
public validator: ValidatorFn = control => { |
|||
if (!this.error) { |
|||
return null; |
|||
} |
|||
|
|||
const path = getControlPath(control, true); |
|||
|
|||
if (!path) { |
|||
return null; |
|||
} |
|||
|
|||
const value = control.value; |
|||
|
|||
const current = this.values[path]; |
|||
|
|||
if (current && current.value !== value) { |
|||
this.values[path] = { value }; |
|||
return null; |
|||
} |
|||
|
|||
const errors: string[] = []; |
|||
|
|||
for (const details of this.error.details) { |
|||
for (const property of details.properties) { |
|||
if (property.startsWith(path)) { |
|||
const subProperty = property.substr(path.length); |
|||
|
|||
const first = subProperty[0]; |
|||
|
|||
if (!first) { |
|||
errors.push(details.message); |
|||
break; |
|||
} else if (first === '[') { |
|||
errors.push(`${subProperty}: ${details.message}`); |
|||
break; |
|||
} else if (first === '.') { |
|||
errors.push(`${subProperty.substr(1)}: ${details.message}`); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (errors.length > 0) { |
|||
this.values[path] = { value }; |
|||
|
|||
return { |
|||
custom: { |
|||
errors |
|||
} |
|||
}; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public setError(error: ErrorDto | undefined | null) { |
|||
this.values = {}; |
|||
this.error = error; |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AbstractControl, ValidatorFn } from '@angular/forms'; |
|||
import { ErrorDto, Types } from '@app/framework/internal'; |
|||
import { State } from './../../state'; |
|||
import { ErrorValidator } from './error-validator'; |
|||
import { addValidator, getRawValue, hasNonCustomError, updateAll } from './forms-helper'; |
|||
|
|||
export interface FormState { |
|||
// The number of submits.
|
|||
submitCount: number; |
|||
|
|||
// True, when the submitting is in progress.
|
|||
submitting: boolean; |
|||
|
|||
// The current remote error.
|
|||
error?: ErrorDto | null; |
|||
} |
|||
|
|||
export class Form<T extends AbstractControl, TOut, TIn = TOut> { |
|||
private readonly state = new State<FormState>({ submitCount: 0, submitting: false }); |
|||
private readonly errorValidator = new ErrorValidator(); |
|||
|
|||
public submitCount = |
|||
this.state.project(s => s.submitCount); |
|||
|
|||
public submitted = |
|||
this.state.project(s => s.submitCount > 0); |
|||
|
|||
public submitting = |
|||
this.state.project(s => s.submitting); |
|||
|
|||
public error = |
|||
this.state.project(s => s.error); |
|||
|
|||
public get remoteValidator(): ValidatorFn { |
|||
return this.errorValidator.validator; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly form: T |
|||
) { |
|||
addValidator(form, this.errorValidator.validator); |
|||
} |
|||
|
|||
public setEnabled(isEnabled: boolean) { |
|||
if (isEnabled) { |
|||
this.enable(); |
|||
} else { |
|||
this.disable(); |
|||
} |
|||
} |
|||
|
|||
protected enable() { |
|||
this.form.enable(); |
|||
} |
|||
|
|||
protected disable() { |
|||
this.form.disable(); |
|||
} |
|||
|
|||
protected setValue(value?: Partial<TIn>) { |
|||
if (value) { |
|||
this.form.reset(this.transformLoad(value)); |
|||
} else { |
|||
this.form.reset(); |
|||
} |
|||
} |
|||
|
|||
protected transformLoad(value: Partial<TIn>): any { |
|||
return value; |
|||
} |
|||
|
|||
protected transformSubmit(value: any): TOut { |
|||
return value; |
|||
} |
|||
|
|||
public load(value: Partial<TIn> | undefined) { |
|||
this.state.resetState(); |
|||
|
|||
this.setValue(value); |
|||
} |
|||
|
|||
public submit(): TOut | null { |
|||
this.updateSubmitState(null, true); |
|||
|
|||
this.form.markAllAsTouched(); |
|||
|
|||
if (!hasNonCustomError(this.form)) { |
|||
const value = this.transformSubmit(getRawValue(this.form)); |
|||
|
|||
if (value) { |
|||
this.disable(); |
|||
} |
|||
|
|||
return value; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public submitCompleted(options?: { newValue?: TOut, noReset?: boolean }) { |
|||
this.updateSubmitState(null, false); |
|||
|
|||
this.enable(); |
|||
|
|||
if (options && options.noReset) { |
|||
this.form.markAsPristine(); |
|||
} else { |
|||
this.setValue(options?.newValue); |
|||
} |
|||
} |
|||
|
|||
public submitFailed(errorOrMessage?: string | ErrorDto, replaceDetails = true) { |
|||
this.updateSubmitState(errorOrMessage, false, replaceDetails); |
|||
|
|||
this.enable(); |
|||
} |
|||
|
|||
private updateSubmitState(errorOrMessage: string | ErrorDto | null | undefined, submitting: boolean, replaceDetails = true) { |
|||
const error = getError(errorOrMessage); |
|||
|
|||
this.state.next(s => ({ |
|||
submitCount: s.submitCount + (submitting ? 1 : 0), |
|||
submitting, |
|||
error |
|||
})); |
|||
|
|||
if (replaceDetails) { |
|||
this.errorValidator.setError(error); |
|||
|
|||
updateAll(this.form); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function getError(error?: string | ErrorDto | null): ErrorDto | undefined | null { |
|||
if (Types.isString(error)) { |
|||
return new ErrorDto(500, error); |
|||
} |
|||
|
|||
return error; |
|||
} |
|||
Loading…
Reference in new issue