Handle async validators

This commit is contained in:
Chocobozzz 2021-12-29 15:33:24 +01:00
parent e2aeb8ad0f
commit cc4bf76c13
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
15 changed files with 82 additions and 58 deletions

View File

@ -63,11 +63,10 @@ describe('Plugins', () => {
const checkbox = await getPluginCheckbox() const checkbox = await getPluginCheckbox()
await checkbox.click() await checkbox.click()
await browserSleep(5000)
await expectSubmitState({ disabled: true }) await expectSubmitState({ disabled: true })
const error = await $('.form-error*=Should be enabled') const error = await $('.form-error*=Should be enabled')
expect(await error.isDisplayed()).toBeTruthy() expect(await error.isDisplayed()).toBeTruthy()
}) })

View File

@ -28,3 +28,7 @@
font-size: 13px; font-size: 13px;
font-weight: $font-semibold; font-weight: $font-semibold;
} }
.alert {
margin-top: 15px;
}

View File

@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { SelectChannelItem } from 'src/types/select-options-item.model' import { SelectChannelItem } from 'src/types/select-options-item.model'
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms' import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
import { HooksService, PluginService, ServerService } from '@app/core' import { HooksService, PluginService, ServerService } from '@app/core'
import { removeElementFromArray } from '@app/helpers' import { removeElementFromArray } from '@app/helpers'
import { BuildFormValidator } from '@app/shared/form-validators' import { BuildFormValidator } from '@app/shared/form-validators'
@ -309,10 +309,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
for (const setting of this.pluginFields) { for (const setting of this.pluginFields) {
await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions) await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions)
const validator = (control: AbstractControl): ValidationErrors | null => { const validator = async (control: AbstractControl) => {
if (!setting.commonOptions.error) return null if (!setting.commonOptions.error) return null
const error = setting.commonOptions.error({ formValues: this.form.value, value: control.value }) const error = await setting.commonOptions.error({ formValues: this.form.value, value: control.value })
return error?.error ? { [setting.commonOptions.name]: error.text } : null return error?.error ? { [setting.commonOptions.name]: error.text } : null
} }
@ -320,7 +320,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
const name = setting.commonOptions.name const name = setting.commonOptions.name
pluginObj[name] = { pluginObj[name] = {
VALIDATORS: [ validator ], ASYNC_VALIDATORS: [ validator ],
VALIDATORS: [],
MESSAGES: {} MESSAGES: {}
} }
@ -342,6 +343,9 @@ export class VideoEditComponent implements OnInit, OnDestroy {
this.cd.detectChanges() this.cd.detectChanges()
this.pluginFieldsAdded.emit() this.pluginFieldsAdded.emit()
// Plugins may need other control values to calculate potential errors
this.form.valueChanges.subscribe(() => this.formValidatorService.updateTreeValidity(this.pluginDataFormGroup))
} }
private trackPrivacyChange () { private trackPrivacyChange () {

View File

@ -110,10 +110,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
}) })
} }
updateSecondStep () { async updateSecondStep () {
if (this.checkForm() === false) { if (!await this.isFormValid()) return
return
}
const video = new VideoEdit() const video = new VideoEdit()
video.patch(this.form.value) video.patch(this.form.value)

View File

@ -123,10 +123,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
}) })
} }
updateSecondStep () { async updateSecondStep () {
if (this.checkForm() === false) { if (!await this.isFormValid()) return
return
}
this.video.patch(this.form.value) this.video.patch(this.form.value)

View File

@ -124,10 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
}) })
} }
updateSecondStep () { async updateSecondStep () {
if (this.checkForm() === false) { if (!await this.isFormValid()) return
return
}
this.video.patch(this.form.value) this.video.patch(this.form.value)

View File

@ -60,12 +60,6 @@ export abstract class VideoSend extends FormReactive implements OnInit {
}) })
} }
checkForm () {
this.forceCheck()
return this.form.valid
}
protected updateVideoAndCaptions (video: VideoEdit) { protected updateVideoAndCaptions (video: VideoEdit) {
this.loadingBar.useRef().start() this.loadingBar.useRef().start()
@ -80,4 +74,11 @@ export abstract class VideoSend extends FormReactive implements OnInit {
}) })
) )
} }
protected async isFormValid () {
await this.waitPendingCheck()
this.forceCheck()
return this.form.valid
}
} }

View File

@ -226,7 +226,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
} }
isPublishingButtonDisabled () { isPublishingButtonDisabled () {
return !this.checkForm() || return !this.form.valid ||
this.isUpdatingVideo === true || this.isUpdatingVideo === true ||
this.videoUploaded !== true || this.videoUploaded !== true ||
!this.videoUploadedIds.id !this.videoUploadedIds.id
@ -239,10 +239,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
return $localize`Upload ${videofile.name}` return $localize`Upload ${videofile.name}`
} }
updateSecondStep () { async updateSecondStep () {
if (this.isPublishingButtonDisabled()) { if (!await this.isFormValid()) return
return if (this.isPublishingButtonDisabled()) return
}
const video = new VideoEdit() const video = new VideoEdit()
video.patch(this.form.value) video.patch(this.form.value)

View File

@ -91,12 +91,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
return { canDeactivate: this.formChanged === false, text } return { canDeactivate: this.formChanged === false, text }
} }
checkForm () {
this.forceCheck()
return this.form.valid
}
isWaitTranscodingEnabled () { isWaitTranscodingEnabled () {
if (this.videoDetails.getFiles().length > 1) { // Already transcoded if (this.videoDetails.getFiles().length > 1) { // Already transcoded
return false return false
@ -109,8 +103,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
return true return true
} }
update () { async update () {
if (this.checkForm() === false || this.isUpdatingVideo === true) { await this.waitPendingCheck()
this.forceCheck()
if (!this.form.valid || this.isUpdatingVideo === true) {
return return
} }

View File

@ -97,7 +97,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
} }
onValidKey () { onValidKey () {
this.check() this.forceCheck()
if (!this.form.valid) return if (!this.form.valid) return
this.formValidated() this.formValidated()

View File

@ -1,7 +1,9 @@
import { ValidatorFn } from '@angular/forms' import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'
export type BuildFormValidator = { export type BuildFormValidator = {
VALIDATORS: ValidatorFn[] VALIDATORS: ValidatorFn[]
ASYNC_VALIDATORS?: AsyncValidatorFn[]
MESSAGES: { [ name: string ]: string } MESSAGES: { [ name: string ]: string }
} }

View File

@ -1,4 +1,6 @@
import { FormGroup } from '@angular/forms' import { FormGroup } from '@angular/forms'
import { wait } from '@root-helpers/utils'
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
import { FormValidatorService } from './form-validator.service' import { FormValidatorService } from './form-validator.service'
@ -22,30 +24,42 @@ export abstract class FormReactive {
this.formErrors = formErrors this.formErrors = formErrors
this.validationMessages = validationMessages this.validationMessages = validationMessages
this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)) this.form.statusChanges.subscribe(async status => {
// FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
await this.waitPendingCheck()
this.onStatusChanged(this.form, this.formErrors, this.validationMessages)
})
}
protected async waitPendingCheck () {
if (this.form.status !== 'PENDING') return
// FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
// return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING')))
// So we have to fallback to active wait :/
do {
await wait(10)
} while (this.form.status === 'PENDING')
} }
protected forceCheck () { protected forceCheck () {
return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true) this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
} }
protected check () { private onStatusChanged (
return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
}
private onValueChanged (
form: FormGroup, form: FormGroup,
formErrors: FormReactiveErrors, formErrors: FormReactiveErrors,
validationMessages: FormReactiveValidationMessages, validationMessages: FormReactiveValidationMessages,
forceCheck = false onlyDirty = true
) { ) {
for (const field of Object.keys(formErrors)) { for (const field of Object.keys(formErrors)) {
if (formErrors[field] && typeof formErrors[field] === 'object') { if (formErrors[field] && typeof formErrors[field] === 'object') {
this.onValueChanged( this.onStatusChanged(
form.controls[field] as FormGroup, form.controls[field] as FormGroup,
formErrors[field] as FormReactiveErrors, formErrors[field] as FormReactiveErrors,
validationMessages[field] as FormReactiveValidationMessages, validationMessages[field] as FormReactiveValidationMessages
forceCheck
) )
continue continue
} }
@ -56,8 +70,7 @@ export abstract class FormReactive {
if (control.dirty) this.formChanged = true if (control.dirty) this.formChanged = true
if (forceCheck) control.updateValueAndValidity({ emitEvent: false }) if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
if (!control || !control.dirty || !control.enabled || control.valid) continue
const staticMessages = validationMessages[field] const staticMessages = validationMessages[field]
for (const key of Object.keys(control.errors)) { for (const key of Object.keys(control.errors)) {
@ -65,11 +78,10 @@ export abstract class FormReactive {
// Try to find error message in static validation messages first // Try to find error message in static validation messages first
// Then check if the validator returns a string that is the error // Then check if the validator returns a string that is the error
if (typeof formErrorValue === 'boolean') formErrors[field] += staticMessages[key] + ' ' if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key] else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
else throw new Error('Form error value of ' + field + ' is invalid') else throw new Error('Form error value of ' + field + ' is invalid')
} }
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive'
@ -68,11 +68,23 @@ export class FormValidatorService {
form.addControl( form.addControl(
name, name,
new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[]) new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[], field?.ASYNC_VALIDATORS as AsyncValidatorFn[])
) )
} }
} }
updateTreeValidity (group: FormGroup | FormArray): void {
for (const key of Object.keys(group.controls)) {
const abstractControl = group.controls[key] as FormControl
if (abstractControl instanceof FormGroup || abstractControl instanceof FormArray) {
this.updateTreeValidity(abstractControl)
} else {
abstractControl.updateValueAndValidity({ emitEvent: false })
}
}
}
private isRecursiveField (field: any) { private isRecursiveField (field: any) {
return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
} }

View File

@ -27,7 +27,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
} }
onValidKey () { onValidKey () {
this.check() this.forceCheck()
if (!this.form.valid) return if (!this.form.valid) return
this.formValidated() this.formValidated()

View File

@ -19,7 +19,7 @@ export type RegisterClientFormFieldOptions = {
// Return undefined | null if there is no error or return a string with the detailed error // Return undefined | null if there is no error or return a string with the detailed error
// Not supported by plugin setting registration // Not supported by plugin setting registration
error?: (options: any) => { error: boolean, text?: string } error?: (options: any) => Promise<{ error: boolean, text?: string }>
} }
export interface RegisterClientVideoFieldOptions { export interface RegisterClientVideoFieldOptions {