diff options
author | Chocobozzz <me@florianbigard.com> | 2021-12-29 15:33:24 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-01-03 14:20:52 +0100 |
commit | cc4bf76c13e38e9065d49161b6e0485657424577 (patch) | |
tree | d7ecc6bd58037c41587eb911776b676592985cd1 /client/src/app | |
parent | e2aeb8ad0f3055d54ac416ec5908d26b70aac4be (diff) | |
download | PeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.tar.gz PeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.tar.zst PeerTube-cc4bf76c13e38e9065d49161b6e0485657424577.zip |
Handle async validators
Diffstat (limited to 'client/src/app')
13 files changed, 79 insertions, 54 deletions
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' | |||
2 | import { map } from 'rxjs/operators' | 2 | import { map } from 'rxjs/operators' |
3 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 3 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
4 | import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 4 | import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
5 | import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms' | 5 | import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms' |
6 | import { HooksService, PluginService, ServerService } from '@app/core' | 6 | import { HooksService, PluginService, ServerService } from '@app/core' |
7 | import { removeElementFromArray } from '@app/helpers' | 7 | import { removeElementFromArray } from '@app/helpers' |
8 | import { BuildFormValidator } from '@app/shared/form-validators' | 8 | import { 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 @@ | |||
1 | import { ValidatorFn } from '@angular/forms' | 1 | import { AsyncValidatorFn, ValidatorFn } from '@angular/forms' |
2 | 2 | ||
3 | export type BuildFormValidator = { | 3 | export 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 | |||
1 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { wait } from '@root-helpers/utils' | ||
2 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
3 | import { FormValidatorService } from './form-validator.service' | 5 | import { 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 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' | 2 | import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' |
3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
4 | import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' | 4 | import { 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() |