aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-12-29 15:33:24 +0100
committerChocobozzz <me@florianbigard.com>2022-01-03 14:20:52 +0100
commitcc4bf76c13e38e9065d49161b6e0485657424577 (patch)
treed7ecc6bd58037c41587eb911776b676592985cd1
parente2aeb8ad0f3055d54ac416ec5908d26b70aac4be (diff)
downloadPeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.tar.gz
PeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.tar.zst
PeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.zip
Handle async validators
-rw-r--r--client/e2e/src/suites-local/plugins.e2e-spec.ts3
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts12
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts9
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts13
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts2
-rw-r--r--client/src/app/shared/form-validators/form-validator.model.ts4
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.ts40
-rw-r--r--client/src/app/shared/shared-forms/form-validator.service.ts16
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts2
-rw-r--r--shared/models/plugins/client/register-client-form-field.model.ts2
15 files changed, 81 insertions, 57 deletions
diff --git a/client/e2e/src/suites-local/plugins.e2e-spec.ts b/client/e2e/src/suites-local/plugins.e2e-spec.ts
index 14802c1ca..55f020c44 100644
--- a/client/e2e/src/suites-local/plugins.e2e-spec.ts
+++ b/client/e2e/src/suites-local/plugins.e2e-spec.ts
@@ -63,11 +63,10 @@ describe('Plugins', () => {
63 const checkbox = await getPluginCheckbox() 63 const checkbox = await getPluginCheckbox()
64 await checkbox.click() 64 await checkbox.click()
65 65
66 await browserSleep(5000)
67
68 await expectSubmitState({ disabled: true }) 66 await expectSubmitState({ disabled: true })
69 67
70 const error = await $('.form-error*=Should be enabled') 68 const error = await $('.form-error*=Should be enabled')
69
71 expect(await error.isDisplayed()).toBeTruthy() 70 expect(await error.isDisplayed()).toBeTruthy()
72 }) 71 })
73 72
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
index 8bb710fc2..10401e9df 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
@@ -28,3 +28,7 @@
28 font-size: 13px; 28 font-size: 13px;
29 font-weight: $font-semibold; 29 font-weight: $font-semibold;
30} 30}
31
32.alert {
33 margin-top: 15px;
34}
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 8ce36121d..be3bbe9be 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
2import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
3import { SelectChannelItem } from 'src/types/select-options-item.model' 3import { SelectChannelItem } from 'src/types/select-options-item.model'
4import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 4import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
5import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms' 5import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
6import { HooksService, PluginService, ServerService } from '@app/core' 6import { HooksService, PluginService, ServerService } from '@app/core'
7import { removeElementFromArray } from '@app/helpers' 7import { removeElementFromArray } from '@app/helpers'
8import { BuildFormValidator } from '@app/shared/form-validators' 8import { BuildFormValidator } from '@app/shared/form-validators'
@@ -309,10 +309,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
309 for (const setting of this.pluginFields) { 309 for (const setting of this.pluginFields) {
310 await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions) 310 await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions)
311 311
312 const validator = (control: AbstractControl): ValidationErrors | null => { 312 const validator = async (control: AbstractControl) => {
313 if (!setting.commonOptions.error) return null 313 if (!setting.commonOptions.error) return null
314 314
315 const error = setting.commonOptions.error({ formValues: this.form.value, value: control.value }) 315 const error = await setting.commonOptions.error({ formValues: this.form.value, value: control.value })
316 316
317 return error?.error ? { [setting.commonOptions.name]: error.text } : null 317 return error?.error ? { [setting.commonOptions.name]: error.text } : null
318 } 318 }
@@ -320,7 +320,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
320 const name = setting.commonOptions.name 320 const name = setting.commonOptions.name
321 321
322 pluginObj[name] = { 322 pluginObj[name] = {
323 VALIDATORS: [ validator ], 323 ASYNC_VALIDATORS: [ validator ],
324 VALIDATORS: [],
324 MESSAGES: {} 325 MESSAGES: {}
325 } 326 }
326 327
@@ -342,6 +343,9 @@ export class VideoEditComponent implements OnInit, OnDestroy {
342 343
343 this.cd.detectChanges() 344 this.cd.detectChanges()
344 this.pluginFieldsAdded.emit() 345 this.pluginFieldsAdded.emit()
346
347 // Plugins may need other control values to calculate potential errors
348 this.form.valueChanges.subscribe(() => this.formValidatorService.updateTreeValidity(this.pluginDataFormGroup))
345 } 349 }
346 350
347 private trackPrivacyChange () { 351 private trackPrivacyChange () {
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 46a7ebb0b..fde8c884b 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -110,10 +110,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
110 }) 110 })
111 } 111 }
112 112
113 updateSecondStep () { 113 async updateSecondStep () {
114 if (this.checkForm() === false) { 114 if (!await this.isFormValid()) return
115 return
116 }
117 115
118 const video = new VideoEdit() 116 const video = new VideoEdit()
119 video.patch(this.form.value) 117 video.patch(this.form.value)
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 5e758910e..c369ba2b7 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -123,10 +123,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
123 }) 123 })
124 } 124 }
125 125
126 updateSecondStep () { 126 async updateSecondStep () {
127 if (this.checkForm() === false) { 127 if (!await this.isFormValid()) return
128 return
129 }
130 128
131 this.video.patch(this.form.value) 129 this.video.patch(this.form.value)
132 130
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
index 2ea70ed55..0c78669c1 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -124,10 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
124 }) 124 })
125 } 125 }
126 126
127 updateSecondStep () { 127 async updateSecondStep () {
128 if (this.checkForm() === false) { 128 if (!await this.isFormValid()) return
129 return
130 }
131 129
132 this.video.patch(this.form.value) 130 this.video.patch(this.form.value)
133 131
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
index 5e086ef42..3d0e1bf2a 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
@@ -60,12 +60,6 @@ export abstract class VideoSend extends FormReactive implements OnInit {
60 }) 60 })
61 } 61 }
62 62
63 checkForm () {
64 this.forceCheck()
65
66 return this.form.valid
67 }
68
69 protected updateVideoAndCaptions (video: VideoEdit) { 63 protected updateVideoAndCaptions (video: VideoEdit) {
70 this.loadingBar.useRef().start() 64 this.loadingBar.useRef().start()
71 65
@@ -80,4 +74,11 @@ export abstract class VideoSend extends FormReactive implements OnInit {
80 }) 74 })
81 ) 75 )
82 } 76 }
77
78 protected async isFormValid () {
79 await this.waitPendingCheck()
80 this.forceCheck()
81
82 return this.form.valid
83 }
83} 84}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index fa5800897..2251b0511 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -226,7 +226,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
226 } 226 }
227 227
228 isPublishingButtonDisabled () { 228 isPublishingButtonDisabled () {
229 return !this.checkForm() || 229 return !this.form.valid ||
230 this.isUpdatingVideo === true || 230 this.isUpdatingVideo === true ||
231 this.videoUploaded !== true || 231 this.videoUploaded !== true ||
232 !this.videoUploadedIds.id 232 !this.videoUploadedIds.id
@@ -239,10 +239,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
239 return $localize`Upload ${videofile.name}` 239 return $localize`Upload ${videofile.name}`
240 } 240 }
241 241
242 updateSecondStep () { 242 async updateSecondStep () {
243 if (this.isPublishingButtonDisabled()) { 243 if (!await this.isFormValid()) return
244 return 244 if (this.isPublishingButtonDisabled()) return
245 }
246 245
247 const video = new VideoEdit() 246 const video = new VideoEdit()
248 video.patch(this.form.value) 247 video.patch(this.form.value)
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index e44aea10a..5e4955f6a 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -91,12 +91,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
91 return { canDeactivate: this.formChanged === false, text } 91 return { canDeactivate: this.formChanged === false, text }
92 } 92 }
93 93
94 checkForm () {
95 this.forceCheck()
96
97 return this.form.valid
98 }
99
100 isWaitTranscodingEnabled () { 94 isWaitTranscodingEnabled () {
101 if (this.videoDetails.getFiles().length > 1) { // Already transcoded 95 if (this.videoDetails.getFiles().length > 1) { // Already transcoded
102 return false 96 return false
@@ -109,8 +103,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
109 return true 103 return true
110 } 104 }
111 105
112 update () { 106 async update () {
113 if (this.checkForm() === false || this.isUpdatingVideo === true) { 107 await this.waitPendingCheck()
108 this.forceCheck()
109
110 if (!this.form.valid || this.isUpdatingVideo === true) {
114 return 111 return
115 } 112 }
116 113
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
index 71fb127f6..85da83a4c 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
@@ -97,7 +97,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
97 } 97 }
98 98
99 onValidKey () { 99 onValidKey () {
100 this.check() 100 this.forceCheck()
101 if (!this.form.valid) return 101 if (!this.form.valid) return
102 102
103 this.formValidated() 103 this.formValidated()
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts
index 6f2472ccd..31c253b9b 100644
--- a/client/src/app/shared/form-validators/form-validator.model.ts
+++ b/client/src/app/shared/form-validators/form-validator.model.ts
@@ -1,7 +1,9 @@
1import { ValidatorFn } from '@angular/forms' 1import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'
2 2
3export type BuildFormValidator = { 3export type BuildFormValidator = {
4 VALIDATORS: ValidatorFn[] 4 VALIDATORS: ValidatorFn[]
5 ASYNC_VALIDATORS?: AsyncValidatorFn[]
6
5 MESSAGES: { [ name: string ]: string } 7 MESSAGES: { [ name: string ]: string }
6} 8}
7 9
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts
index 30b59c141..07a12c6f6 100644
--- a/client/src/app/shared/shared-forms/form-reactive.ts
+++ b/client/src/app/shared/shared-forms/form-reactive.ts
@@ -1,4 +1,6 @@
1
1import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { wait } from '@root-helpers/utils'
2import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 4import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
3import { FormValidatorService } from './form-validator.service' 5import { FormValidatorService } from './form-validator.service'
4 6
@@ -22,30 +24,42 @@ export abstract class FormReactive {
22 this.formErrors = formErrors 24 this.formErrors = formErrors
23 this.validationMessages = validationMessages 25 this.validationMessages = validationMessages
24 26
25 this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)) 27 this.form.statusChanges.subscribe(async status => {
28 // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
29 await this.waitPendingCheck()
30
31 this.onStatusChanged(this.form, this.formErrors, this.validationMessages)
32 })
26 } 33 }
27 34
28 protected forceCheck () { 35 protected async waitPendingCheck () {
29 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true) 36 if (this.form.status !== 'PENDING') return
37
38 // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
39 // return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING')))
40 // So we have to fallback to active wait :/
41
42 do {
43 await wait(10)
44 } while (this.form.status === 'PENDING')
30 } 45 }
31 46
32 protected check () { 47 protected forceCheck () {
33 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false) 48 this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false)
34 } 49 }
35 50
36 private onValueChanged ( 51 private onStatusChanged (
37 form: FormGroup, 52 form: FormGroup,
38 formErrors: FormReactiveErrors, 53 formErrors: FormReactiveErrors,
39 validationMessages: FormReactiveValidationMessages, 54 validationMessages: FormReactiveValidationMessages,
40 forceCheck = false 55 onlyDirty = true
41 ) { 56 ) {
42 for (const field of Object.keys(formErrors)) { 57 for (const field of Object.keys(formErrors)) {
43 if (formErrors[field] && typeof formErrors[field] === 'object') { 58 if (formErrors[field] && typeof formErrors[field] === 'object') {
44 this.onValueChanged( 59 this.onStatusChanged(
45 form.controls[field] as FormGroup, 60 form.controls[field] as FormGroup,
46 formErrors[field] as FormReactiveErrors, 61 formErrors[field] as FormReactiveErrors,
47 validationMessages[field] as FormReactiveValidationMessages, 62 validationMessages[field] as FormReactiveValidationMessages
48 forceCheck
49 ) 63 )
50 continue 64 continue
51 } 65 }
@@ -56,8 +70,7 @@ export abstract class FormReactive {
56 70
57 if (control.dirty) this.formChanged = true 71 if (control.dirty) this.formChanged = true
58 72
59 if (forceCheck) control.updateValueAndValidity({ emitEvent: false }) 73 if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
60 if (!control || !control.dirty || !control.enabled || control.valid) continue
61 74
62 const staticMessages = validationMessages[field] 75 const staticMessages = validationMessages[field]
63 for (const key of Object.keys(control.errors)) { 76 for (const key of Object.keys(control.errors)) {
@@ -65,11 +78,10 @@ export abstract class FormReactive {
65 78
66 // Try to find error message in static validation messages first 79 // Try to find error message in static validation messages first
67 // Then check if the validator returns a string that is the error 80 // Then check if the validator returns a string that is the error
68 if (typeof formErrorValue === 'boolean') formErrors[field] += staticMessages[key] + ' ' 81 if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
69 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key] 82 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
70 else throw new Error('Form error value of ' + field + ' is invalid') 83 else throw new Error('Form error value of ' + field + ' is invalid')
71 } 84 }
72 } 85 }
73 } 86 }
74
75} 87}
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts
index 055fbb2d9..0fe50ac9b 100644
--- a/client/src/app/shared/shared-forms/form-validator.service.ts
+++ b/client/src/app/shared/shared-forms/form-validator.service.ts
@@ -1,5 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' 2import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' 4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive'
5 5
@@ -68,11 +68,23 @@ export class FormValidatorService {
68 68
69 form.addControl( 69 form.addControl(
70 name, 70 name,
71 new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[]) 71 new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[], field?.ASYNC_VALIDATORS as AsyncValidatorFn[])
72 ) 72 )
73 } 73 }
74 } 74 }
75 75
76 updateTreeValidity (group: FormGroup | FormArray): void {
77 for (const key of Object.keys(group.controls)) {
78 const abstractControl = group.controls[key] as FormControl
79
80 if (abstractControl instanceof FormGroup || abstractControl instanceof FormArray) {
81 this.updateTreeValidity(abstractControl)
82 } else {
83 abstractControl.updateValueAndValidity({ emitEvent: false })
84 }
85 }
86 }
87
76 private isRecursiveField (field: any) { 88 private isRecursiveField (field: any) {
77 return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS 89 return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
78 } 90 }
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
index a951134eb..369692715 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
@@ -27,7 +27,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
27 } 27 }
28 28
29 onValidKey () { 29 onValidKey () {
30 this.check() 30 this.forceCheck()
31 if (!this.form.valid) return 31 if (!this.form.valid) return
32 32
33 this.formValidated() 33 this.formValidated()
diff --git a/shared/models/plugins/client/register-client-form-field.model.ts b/shared/models/plugins/client/register-client-form-field.model.ts
index 30fd63266..153c4a6ea 100644
--- a/shared/models/plugins/client/register-client-form-field.model.ts
+++ b/shared/models/plugins/client/register-client-form-field.model.ts
@@ -19,7 +19,7 @@ export type RegisterClientFormFieldOptions = {
19 19
20 // Return undefined | null if there is no error or return a string with the detailed error 20 // Return undefined | null if there is no error or return a string with the detailed error
21 // Not supported by plugin setting registration 21 // Not supported by plugin setting registration
22 error?: (options: any) => { error: boolean, text?: string } 22 error?: (options: any) => Promise<{ error: boolean, text?: string }>
23} 23}
24 24
25export interface RegisterClientVideoFieldOptions { 25export interface RegisterClientVideoFieldOptions {