Handle async validators
This commit is contained in:
parent
e2aeb8ad0f
commit
cc4bf76c13
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -28,3 +28,7 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user