]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Handle async validators
authorChocobozzz <me@florianbigard.com>
Wed, 29 Dec 2021 14:33:24 +0000 (15:33 +0100)
committerChocobozzz <me@florianbigard.com>
Mon, 3 Jan 2022 13:20:52 +0000 (14:20 +0100)
15 files changed:
client/e2e/src/suites-local/plugins.e2e-spec.ts
client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
client/src/app/+videos/+video-edit/shared/video-edit.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-send.ts
client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
client/src/app/+videos/+video-edit/video-update.component.ts
client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
client/src/app/shared/form-validators/form-validator.model.ts
client/src/app/shared/shared-forms/form-reactive.ts
client/src/app/shared/shared-forms/form-validator.service.ts
client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
shared/models/plugins/client/register-client-form-field.model.ts

index 14802c1ca286eef2a7a24e353276fbb5d0817058..55f020c448f14ce8bcdd7a2bafc80dd95434dfeb 100644 (file)
@@ -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()
   })
 
index 8bb710fc22f67637e840f2fe338b1da5c8e851ce..10401e9dfb1d8176486ee7ddfc79eb104fbd6bbe 100644 (file)
@@ -28,3 +28,7 @@
   font-size: 13px;
   font-weight: $font-semibold;
 }
+
+.alert {
+  margin-top: 15px;
+}
index 8ce36121d0b65408a67cd57975308afa9de080ee..be3bbe9be844768427bd2415672de949ff3c7b78 100644 (file)
@@ -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 () {
index 46a7ebb0b97f18afdb77de71d0c5d0b297f34829..fde8c884b9b9e25edb90c7cae11b076141c066e4 100644 (file)
@@ -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)
index 5e758910ecf77f8c00cb5e4c557bcd0ab1591772..c369ba2b7a156efc81ab8602477dc9b89585834e 100644 (file)
@@ -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)
 
index 2ea70ed5566595446de76204cda098a00054cb97..0c78669c127a0da8309d64ff0f374d3034399f77 100644 (file)
@@ -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)
 
index 5e086ef42f4aa96d6d7cc6f5b0f96a63b2b754c0..3d0e1bf2af3cf2573c7e3f0b7589ef6a6c10bce5 100644 (file)
@@ -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
+  }
 }
index fa58008971dbfd82ea2411b78ceefc2bccba5fc2..2251b05119ff8079b99d1ab6c2eb451f2ec65587 100644 (file)
@@ -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)
index e44aea10ad978136c697c6d6170d8fedb582852e..5e4955f6ae878557b50b2ef44c9aa1926130ba46 100644 (file)
@@ -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
     }
 
index 71fb127f6c5cc8c6204bd705d38048efd42ae3f0..85da83a4c50d9ca95befac36426ffa3146a96a2f 100644 (file)
@@ -97,7 +97,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
   }
 
   onValidKey () {
-    this.check()
+    this.forceCheck()
     if (!this.form.valid) return
 
     this.formValidated()
index 6f2472ccded1942403e019ffd6addf0ed5b6193f..31c253b9b61e317e685872896d056573e8826276 100644 (file)
@@ -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 }
 }
 
index 30b59c141d29f867a6d1f417f61f58993116b488..07a12c6f69003aec5c65024a192870a358e768ba 100644 (file)
@@ -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 forceCheck () {
-    return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true)
+  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 check () {
-    return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
+  protected forceCheck () {
+    this.onStatusChanged(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')
       }
     }
   }
-
 }
index 055fbb2d956a0a64ebc24f51fbc5300ecaad1ff8..0fe50ac9bf7bc4be69be0907db5fc6bdb12e34b5 100644 (file)
@@ -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
   }
index a951134eb8223d39faf62deb2fb0d48716cd1219..36969271530da72394ee8e52931bab9a2cf43ded 100644 (file)
@@ -27,7 +27,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
   }
 
   onValidKey () {
-    this.check()
+    this.forceCheck()
     if (!this.form.valid) return
 
     this.formValidated()
index 30fd632669894e3b52c63c8b7fd6281b31e655b3..153c4a6eac90eae7ff7f037ec07df7b41bbec410 100644 (file)
@@ -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 {