Handle async validators
This commit is contained in:
parent
e2aeb8ad0f
commit
cc4bf76c13
|
@ -63,11 +63,10 @@ describe('Plugins', () => {
|
|||
const checkbox = await getPluginCheckbox()
|
||||
await checkbox.click()
|
||||
|
||||
await browserSleep(5000)
|
||||
|
||||
await expectSubmitState({ disabled: true })
|
||||
|
||||
const error = await $('.form-error*=Should be enabled')
|
||||
|
||||
expect(await error.isDisplayed()).toBeTruthy()
|
||||
})
|
||||
|
||||
|
|
|
@ -28,3 +28,7 @@
|
|||
font-size: 13px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
|
|||
import { map } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
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 { removeElementFromArray } from '@app/helpers'
|
||||
import { BuildFormValidator } from '@app/shared/form-validators'
|
||||
|
@ -309,10 +309,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
for (const setting of this.pluginFields) {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -320,7 +320,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
const name = setting.commonOptions.name
|
||||
|
||||
pluginObj[name] = {
|
||||
VALIDATORS: [ validator ],
|
||||
ASYNC_VALIDATORS: [ validator ],
|
||||
VALIDATORS: [],
|
||||
MESSAGES: {}
|
||||
}
|
||||
|
||||
|
@ -342,6 +343,9 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.cd.detectChanges()
|
||||
this.pluginFieldsAdded.emit()
|
||||
|
||||
// Plugins may need other control values to calculate potential errors
|
||||
this.form.valueChanges.subscribe(() => this.formValidatorService.updateTreeValidity(this.pluginDataFormGroup))
|
||||
}
|
||||
|
||||
private trackPrivacyChange () {
|
||||
|
|
|
@ -110,10 +110,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
|||
})
|
||||
}
|
||||
|
||||
updateSecondStep () {
|
||||
if (this.checkForm() === false) {
|
||||
return
|
||||
}
|
||||
async updateSecondStep () {
|
||||
if (!await this.isFormValid()) return
|
||||
|
||||
const video = new VideoEdit()
|
||||
video.patch(this.form.value)
|
||||
|
|
|
@ -123,10 +123,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
|
|||
})
|
||||
}
|
||||
|
||||
updateSecondStep () {
|
||||
if (this.checkForm() === false) {
|
||||
return
|
||||
}
|
||||
async updateSecondStep () {
|
||||
if (!await this.isFormValid()) return
|
||||
|
||||
this.video.patch(this.form.value)
|
||||
|
||||
|
|
|
@ -124,10 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
|
|||
})
|
||||
}
|
||||
|
||||
updateSecondStep () {
|
||||
if (this.checkForm() === false) {
|
||||
return
|
||||
}
|
||||
async updateSecondStep () {
|
||||
if (!await this.isFormValid()) return
|
||||
|
||||
this.video.patch(this.form.value)
|
||||
|
||||
|
|
|
@ -60,12 +60,6 @@ export abstract class VideoSend extends FormReactive implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
checkForm () {
|
||||
this.forceCheck()
|
||||
|
||||
return this.form.valid
|
||||
}
|
||||
|
||||
protected updateVideoAndCaptions (video: VideoEdit) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,7 +226,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
|
||||
isPublishingButtonDisabled () {
|
||||
return !this.checkForm() ||
|
||||
return !this.form.valid ||
|
||||
this.isUpdatingVideo === true ||
|
||||
this.videoUploaded !== true ||
|
||||
!this.videoUploadedIds.id
|
||||
|
@ -239,10 +239,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
return $localize`Upload ${videofile.name}`
|
||||
}
|
||||
|
||||
updateSecondStep () {
|
||||
if (this.isPublishingButtonDisabled()) {
|
||||
return
|
||||
}
|
||||
async updateSecondStep () {
|
||||
if (!await this.isFormValid()) return
|
||||
if (this.isPublishingButtonDisabled()) return
|
||||
|
||||
const video = new VideoEdit()
|
||||
video.patch(this.form.value)
|
||||
|
|
|
@ -91,12 +91,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
return { canDeactivate: this.formChanged === false, text }
|
||||
}
|
||||
|
||||
checkForm () {
|
||||
this.forceCheck()
|
||||
|
||||
return this.form.valid
|
||||
}
|
||||
|
||||
isWaitTranscodingEnabled () {
|
||||
if (this.videoDetails.getFiles().length > 1) { // Already transcoded
|
||||
return false
|
||||
|
@ -109,8 +103,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
return true
|
||||
}
|
||||
|
||||
update () {
|
||||
if (this.checkForm() === false || this.isUpdatingVideo === true) {
|
||||
async update () {
|
||||
await this.waitPendingCheck()
|
||||
this.forceCheck()
|
||||
|
||||
if (!this.form.valid || this.isUpdatingVideo === true) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
|
|||
}
|
||||
|
||||
onValidKey () {
|
||||
this.check()
|
||||
this.forceCheck()
|
||||
if (!this.form.valid) return
|
||||
|
||||
this.formValidated()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { ValidatorFn } from '@angular/forms'
|
||||
import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'
|
||||
|
||||
export type BuildFormValidator = {
|
||||
VALIDATORS: ValidatorFn[]
|
||||
ASYNC_VALIDATORS?: AsyncValidatorFn[]
|
||||
|
||||
MESSAGES: { [ name: string ]: string }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { wait } from '@root-helpers/utils'
|
||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
||||
import { FormValidatorService } from './form-validator.service'
|
||||
|
||||
|
@ -22,30 +24,42 @@ export abstract class FormReactive {
|
|||
this.formErrors = formErrors
|
||||
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 () {
|
||||
return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true)
|
||||
this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
|
||||
}
|
||||
|
||||
protected check () {
|
||||
return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
|
||||
}
|
||||
|
||||
private onValueChanged (
|
||||
private onStatusChanged (
|
||||
form: FormGroup,
|
||||
formErrors: FormReactiveErrors,
|
||||
validationMessages: FormReactiveValidationMessages,
|
||||
forceCheck = false
|
||||
onlyDirty = true
|
||||
) {
|
||||
for (const field of Object.keys(formErrors)) {
|
||||
if (formErrors[field] && typeof formErrors[field] === 'object') {
|
||||
this.onValueChanged(
|
||||
this.onStatusChanged(
|
||||
form.controls[field] as FormGroup,
|
||||
formErrors[field] as FormReactiveErrors,
|
||||
validationMessages[field] as FormReactiveValidationMessages,
|
||||
forceCheck
|
||||
validationMessages[field] as FormReactiveValidationMessages
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -56,8 +70,7 @@ export abstract class FormReactive {
|
|||
|
||||
if (control.dirty) this.formChanged = true
|
||||
|
||||
if (forceCheck) control.updateValueAndValidity({ emitEvent: false })
|
||||
if (!control || !control.dirty || !control.enabled || control.valid) continue
|
||||
if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
|
||||
|
||||
const staticMessages = validationMessages[field]
|
||||
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
|
||||
// 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 throw new Error('Form error value of ' + field + ' is invalid')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive'
|
||||
|
||||
|
@ -68,11 +68,23 @@ export class FormValidatorService {
|
|||
|
||||
form.addControl(
|
||||
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) {
|
||||
return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
onValidKey () {
|
||||
this.check()
|
||||
this.forceCheck()
|
||||
if (!this.form.valid) return
|
||||
|
||||
this.formValidated()
|
||||
|
|
|
@ -19,7 +19,7 @@ export type RegisterClientFormFieldOptions = {
|
|||
|
||||
// Return undefined | null if there is no error or return a string with the detailed error
|
||||
// Not supported by plugin setting registration
|
||||
error?: (options: any) => { error: boolean, text?: string }
|
||||
error?: (options: any) => Promise<{ error: boolean, text?: string }>
|
||||
}
|
||||
|
||||
export interface RegisterClientVideoFieldOptions {
|
||||
|
|
Loading…
Reference in New Issue