diff options
author | Chocobozzz <me@florianbigard.com> | 2021-12-22 18:02:36 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-12-29 10:10:01 +0100 |
commit | 3c065fe3b3e1385d59ad1980251d14b712648155 (patch) | |
tree | f5f7f1b428b8155a735014304e2d45b9ed92fe07 | |
parent | 61cc1c03bf6f12af7c1b2e2a7d2fdaa563c37b59 (diff) | |
download | PeerTube-3c065fe3b3e1385d59ad1980251d14b712648155.tar.gz PeerTube-3c065fe3b3e1385d59ad1980251d14b712648155.tar.zst PeerTube-3c065fe3b3e1385d59ad1980251d14b712648155.zip |
Enhance plugin video fields
Add video form tab selection
Add ability to display an error
-rw-r--r-- | client/e2e/src/po/admin-plugin.po.ts | 31 | ||||
-rw-r--r-- | client/e2e/src/po/anonymous-settings.po.ts | 2 | ||||
-rw-r--r-- | client/e2e/src/po/my-account.po.ts | 2 | ||||
-rw-r--r-- | client/e2e/src/po/video-upload.po.ts | 21 | ||||
-rw-r--r-- | client/e2e/src/suites-local/plugins.e2e-spec.ts | 79 | ||||
-rw-r--r-- | client/e2e/src/utils/elements.ts | 2 | ||||
-rw-r--r-- | client/src/app/+videos/+video-edit/shared/video-edit.component.html | 13 | ||||
-rw-r--r-- | client/src/app/+videos/+video-edit/shared/video-edit.component.ts | 55 | ||||
-rw-r--r-- | client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts | 4 | ||||
-rw-r--r-- | client/src/app/shared/shared-forms/form-reactive.ts | 19 | ||||
-rw-r--r-- | client/src/app/shared/shared-forms/form-validator.service.ts | 10 | ||||
-rw-r--r-- | shared/models/plugins/client/register-client-form-field.model.ts | 7 | ||||
-rw-r--r-- | support/doc/plugins/guide.md | 17 |
13 files changed, 228 insertions, 34 deletions
diff --git a/client/e2e/src/po/admin-plugin.po.ts b/client/e2e/src/po/admin-plugin.po.ts new file mode 100644 index 000000000..6a3f3cf28 --- /dev/null +++ b/client/e2e/src/po/admin-plugin.po.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import { browserSleep, go } from '../utils' | ||
2 | |||
3 | export class AdminPluginPage { | ||
4 | |||
5 | async navigateToSearch () { | ||
6 | await go('/admin/plugins/search') | ||
7 | |||
8 | await $('my-plugin-search').waitForDisplayed() | ||
9 | } | ||
10 | |||
11 | async search (name: string) { | ||
12 | const input = $('.search-bar input') | ||
13 | await input.waitForDisplayed() | ||
14 | await input.clearValue() | ||
15 | await input.setValue(name) | ||
16 | |||
17 | await browserSleep(1000) | ||
18 | } | ||
19 | |||
20 | async installHelloWorld () { | ||
21 | $('.plugin-name=hello-world').waitForDisplayed() | ||
22 | |||
23 | await $('.card-body my-button[icon=cloud-download]').click() | ||
24 | |||
25 | const submitModalButton = $('.modal-content input[type=submit]') | ||
26 | await submitModalButton.waitForClickable() | ||
27 | await submitModalButton.click() | ||
28 | |||
29 | await $('.card-body my-edit-button').waitForDisplayed() | ||
30 | } | ||
31 | } | ||
diff --git a/client/e2e/src/po/anonymous-settings.po.ts b/client/e2e/src/po/anonymous-settings.po.ts index 180d371fa..21216a8f2 100644 --- a/client/e2e/src/po/anonymous-settings.po.ts +++ b/client/e2e/src/po/anonymous-settings.po.ts | |||
@@ -13,7 +13,7 @@ export class AnonymousSettingsPage { | |||
13 | } | 13 | } |
14 | 14 | ||
15 | async clickOnP2PCheckbox () { | 15 | async clickOnP2PCheckbox () { |
16 | const p2p = getCheckbox('p2pEnabled') | 16 | const p2p = await getCheckbox('p2pEnabled') |
17 | await p2p.waitForClickable() | 17 | await p2p.waitForClickable() |
18 | 18 | ||
19 | await p2p.click() | 19 | await p2p.click() |
diff --git a/client/e2e/src/po/my-account.po.ts b/client/e2e/src/po/my-account.po.ts index 13a764e87..20dafbf06 100644 --- a/client/e2e/src/po/my-account.po.ts +++ b/client/e2e/src/po/my-account.po.ts | |||
@@ -31,7 +31,7 @@ export class MyAccountPage { | |||
31 | } | 31 | } |
32 | 32 | ||
33 | async clickOnP2PCheckbox () { | 33 | async clickOnP2PCheckbox () { |
34 | const p2p = getCheckbox('p2pEnabled') | 34 | const p2p = await getCheckbox('p2pEnabled') |
35 | 35 | ||
36 | await p2p.waitForClickable() | 36 | await p2p.waitForClickable() |
37 | await p2p.scrollIntoView(false) // Avoid issues with fixed header on firefox | 37 | await p2p.scrollIntoView(false) // Avoid issues with fixed header on firefox |
diff --git a/client/e2e/src/po/video-upload.po.ts b/client/e2e/src/po/video-upload.po.ts index 2206b56c3..38395ea2f 100644 --- a/client/e2e/src/po/video-upload.po.ts +++ b/client/e2e/src/po/video-upload.po.ts | |||
@@ -3,7 +3,10 @@ import { getCheckbox, selectCustomSelect } from '../utils' | |||
3 | 3 | ||
4 | export class VideoUploadPage { | 4 | export class VideoUploadPage { |
5 | async navigateTo () { | 5 | async navigateTo () { |
6 | await $('.header .publish-button').click() | 6 | const publishButton = await $('.header .publish-button') |
7 | |||
8 | await publishButton.waitForClickable() | ||
9 | await publishButton.click() | ||
7 | 10 | ||
8 | await $('.upload-video-container').waitForDisplayed() | 11 | await $('.upload-video-container').waitForDisplayed() |
9 | } | 12 | } |
@@ -24,15 +27,17 @@ export class VideoUploadPage { | |||
24 | 27 | ||
25 | // Wait for the upload to finish | 28 | // Wait for the upload to finish |
26 | await browser.waitUntil(async () => { | 29 | await browser.waitUntil(async () => { |
27 | const actionButton = this.getSecondStepSubmitButton().$('.action-button') | 30 | const warning = await $('=Publish will be available when upload is finished').isDisplayed() |
31 | const progress = await $('.progress-bar=100%').isDisplayed() | ||
28 | 32 | ||
29 | const klass = await actionButton.getAttribute('class') | 33 | return !warning && progress |
30 | return !klass.includes('disabled') | ||
31 | }) | 34 | }) |
32 | } | 35 | } |
33 | 36 | ||
34 | setAsNSFW () { | 37 | async setAsNSFW () { |
35 | return getCheckbox('nsfw').click() | 38 | const checkbox = await getCheckbox('nsfw') |
39 | |||
40 | return checkbox.click() | ||
36 | } | 41 | } |
37 | 42 | ||
38 | async validSecondUploadStep (videoName: string) { | 43 | async validSecondUploadStep (videoName: string) { |
@@ -51,6 +56,10 @@ export class VideoUploadPage { | |||
51 | return selectCustomSelect('privacy', 'Public') | 56 | return selectCustomSelect('privacy', 'Public') |
52 | } | 57 | } |
53 | 58 | ||
59 | setAsPrivate () { | ||
60 | return selectCustomSelect('privacy', 'Private') | ||
61 | } | ||
62 | |||
54 | private getSecondStepSubmitButton () { | 63 | private getSecondStepSubmitButton () { |
55 | return $('.submit-container my-button') | 64 | return $('.submit-container my-button') |
56 | } | 65 | } |
diff --git a/client/e2e/src/suites-local/plugins.e2e-spec.ts b/client/e2e/src/suites-local/plugins.e2e-spec.ts new file mode 100644 index 000000000..14802c1ca --- /dev/null +++ b/client/e2e/src/suites-local/plugins.e2e-spec.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | import { AdminPluginPage } from '../po/admin-plugin.po' | ||
2 | import { LoginPage } from '../po/login.po' | ||
3 | import { VideoUploadPage } from '../po/video-upload.po' | ||
4 | import { browserSleep, getCheckbox, waitServerUp } from '../utils' | ||
5 | |||
6 | describe('Plugins', () => { | ||
7 | let videoUploadPage: VideoUploadPage | ||
8 | let loginPage: LoginPage | ||
9 | let adminPluginPage: AdminPluginPage | ||
10 | |||
11 | function getPluginCheckbox () { | ||
12 | return getCheckbox('hello-world-field-4') | ||
13 | } | ||
14 | |||
15 | async function expectSubmitState ({ disabled }: { disabled: boolean }) { | ||
16 | const disabledSubmit = await $('my-button .disabled') | ||
17 | |||
18 | if (disabled) expect(await disabledSubmit.isDisplayed()).toBeTruthy() | ||
19 | else expect(await disabledSubmit.isDisplayed()).toBeFalsy() | ||
20 | } | ||
21 | |||
22 | before(async () => { | ||
23 | await waitServerUp() | ||
24 | }) | ||
25 | |||
26 | beforeEach(async () => { | ||
27 | loginPage = new LoginPage() | ||
28 | videoUploadPage = new VideoUploadPage() | ||
29 | adminPluginPage = new AdminPluginPage() | ||
30 | |||
31 | await browser.maximizeWindow() | ||
32 | }) | ||
33 | |||
34 | it('Should install hello world plugin', async () => { | ||
35 | await loginPage.loginAsRootUser() | ||
36 | |||
37 | await adminPluginPage.navigateToSearch() | ||
38 | await adminPluginPage.search('hello-world') | ||
39 | await adminPluginPage.installHelloWorld() | ||
40 | await browser.refresh() | ||
41 | }) | ||
42 | |||
43 | it('Should have checkbox in video edit page', async () => { | ||
44 | await videoUploadPage.navigateTo() | ||
45 | await videoUploadPage.uploadVideo() | ||
46 | |||
47 | await $('span=Super field 4 in main tab').waitForDisplayed() | ||
48 | |||
49 | const checkbox = await getPluginCheckbox() | ||
50 | expect(await checkbox.isDisplayed()).toBeTruthy() | ||
51 | |||
52 | await expectSubmitState({ disabled: true }) | ||
53 | }) | ||
54 | |||
55 | it('Should check the checkbox and be able to submit the video', async function () { | ||
56 | const checkbox = await getPluginCheckbox() | ||
57 | await checkbox.click() | ||
58 | |||
59 | await expectSubmitState({ disabled: false }) | ||
60 | }) | ||
61 | |||
62 | it('Should uncheck the checkbox and not be able to submit the video', async function () { | ||
63 | const checkbox = await getPluginCheckbox() | ||
64 | await checkbox.click() | ||
65 | |||
66 | await browserSleep(5000) | ||
67 | |||
68 | await expectSubmitState({ disabled: true }) | ||
69 | |||
70 | const error = await $('.form-error*=Should be enabled') | ||
71 | expect(await error.isDisplayed()).toBeTruthy() | ||
72 | }) | ||
73 | |||
74 | it('Should change the privacy and should hide the checkbox', async function () { | ||
75 | await videoUploadPage.setAsPrivate() | ||
76 | |||
77 | await expectSubmitState({ disabled: false }) | ||
78 | }) | ||
79 | }) | ||
diff --git a/client/e2e/src/utils/elements.ts b/client/e2e/src/utils/elements.ts index 315718879..3ffa5defd 100644 --- a/client/e2e/src/utils/elements.ts +++ b/client/e2e/src/utils/elements.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | function getCheckbox (name: string) { | 1 | function getCheckbox (name: string) { |
2 | return $(`my-peertube-checkbox[inputname=${name}] label`) | 2 | return $(`my-peertube-checkbox input[id=${name}]`).parentElement() |
3 | } | 3 | } |
4 | 4 | ||
5 | async function selectCustomSelect (id: string, valueLabel: string) { | 5 | async function selectCustomSelect (id: string, valueLabel: string) { |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index aa88d6c4c..f65cd8883 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -146,6 +146,13 @@ | |||
146 | </ng-template> | 146 | </ng-template> |
147 | </my-peertube-checkbox> | 147 | </my-peertube-checkbox> |
148 | 148 | ||
149 | <ng-container ngbNavItem *ngIf="getPluginsFields('main').length !== 0"> | ||
150 | |||
151 | <div *ngFor="let pluginSetting of getPluginsFields('main')" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)"> | ||
152 | <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors['pluginData']" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field> | ||
153 | </div> | ||
154 | |||
155 | </ng-container> | ||
149 | </div> | 156 | </div> |
150 | </div> | 157 | </div> |
151 | </ng-template> | 158 | </ng-template> |
@@ -339,15 +346,15 @@ | |||
339 | </ng-template> | 346 | </ng-template> |
340 | </ng-container> | 347 | </ng-container> |
341 | 348 | ||
342 | <ng-container ngbNavItem *ngIf="pluginFields.length !== 0"> | 349 | <ng-container ngbNavItem *ngIf="getPluginsFields('plugin-settings').length !== 0"> |
343 | <a ngbNavLink i18n>Plugin settings</a> | 350 | <a ngbNavLink i18n>Plugin settings</a> |
344 | 351 | ||
345 | <ng-template ngbNavContent> | 352 | <ng-template ngbNavContent> |
346 | <div class="row plugin-settings"> | 353 | <div class="row plugin-settings"> |
347 | 354 | ||
348 | <div class="col-md-12 col-xl-8"> | 355 | <div class="col-md-12 col-xl-8"> |
349 | <div *ngFor="let pluginSetting of pluginFields" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)"> | 356 | <div *ngFor="let pluginSetting of getPluginsFields('plugin-settings')" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)"> |
350 | <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field> | 357 | <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors['pluginData']" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field> |
351 | </div> | 358 | </div> |
352 | </div> | 359 | </div> |
353 | 360 | ||
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 2bec933e9..a03005bcb 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 | |||
@@ -1,10 +1,11 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | 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 { 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 { FormArray, FormControl, FormGroup, Validators } from '@angular/forms' | 5 | import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, 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 { | 9 | import { |
9 | VIDEO_CATEGORY_VALIDATOR, | 10 | VIDEO_CATEGORY_VALIDATOR, |
10 | VIDEO_CHANNEL_VALIDATOR, | 11 | VIDEO_CHANNEL_VALIDATOR, |
@@ -101,7 +102,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
101 | private instanceService: InstanceService, | 102 | private instanceService: InstanceService, |
102 | private i18nPrimengCalendarService: I18nPrimengCalendarService, | 103 | private i18nPrimengCalendarService: I18nPrimengCalendarService, |
103 | private ngZone: NgZone, | 104 | private ngZone: NgZone, |
104 | private hooks: HooksService | 105 | private hooks: HooksService, |
106 | private cd: ChangeDetectorRef | ||
105 | ) { | 107 | ) { |
106 | this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() | 108 | this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() |
107 | this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() | 109 | this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() |
@@ -116,7 +118,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
116 | licence: this.serverConfig.defaults.publish.licence, | 118 | licence: this.serverConfig.defaults.publish.licence, |
117 | tags: [] | 119 | tags: [] |
118 | } | 120 | } |
119 | const obj: any = { | 121 | const obj: { [ id: string ]: BuildFormValidator } = { |
120 | name: VIDEO_NAME_VALIDATOR, | 122 | name: VIDEO_NAME_VALIDATOR, |
121 | privacy: VIDEO_PRIVACY_VALIDATOR, | 123 | privacy: VIDEO_PRIVACY_VALIDATOR, |
122 | channelId: VIDEO_CHANNEL_VALIDATOR, | 124 | channelId: VIDEO_CHANNEL_VALIDATOR, |
@@ -138,7 +140,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
138 | saveReplay: null | 140 | saveReplay: null |
139 | } | 141 | } |
140 | 142 | ||
141 | this.formValidatorService.updateForm( | 143 | this.formValidatorService.updateFormGroup( |
142 | this.form, | 144 | this.form, |
143 | this.formErrors, | 145 | this.formErrors, |
144 | this.validationMessages, | 146 | this.validationMessages, |
@@ -275,6 +277,14 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
275 | }) | 277 | }) |
276 | } | 278 | } |
277 | 279 | ||
280 | getPluginsFields (tab: 'main' | 'plugin-settings') { | ||
281 | return this.pluginFields.filter(p => { | ||
282 | const wanted = p.videoFormOptions.tab ?? 'plugin-settings' | ||
283 | |||
284 | return wanted === tab | ||
285 | }) | ||
286 | } | ||
287 | |||
278 | private sortVideoCaptions () { | 288 | private sortVideoCaptions () { |
279 | this.videoCaptions.sort((v1, v2) => { | 289 | this.videoCaptions.sort((v1, v2) => { |
280 | if (v1.language.label < v2.language.label) return -1 | 290 | if (v1.language.label < v2.language.label) return -1 |
@@ -289,15 +299,44 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
289 | 299 | ||
290 | if (this.pluginFields.length === 0) return | 300 | if (this.pluginFields.length === 0) return |
291 | 301 | ||
292 | const obj: any = {} | 302 | const pluginObj: { [ id: string ]: BuildFormValidator } = {} |
303 | const pluginValidationMessages: FormReactiveValidationMessages = {} | ||
304 | const pluginFormErrors: any = {} | ||
305 | const pluginDefaults: any = {} | ||
293 | 306 | ||
294 | for (const setting of this.pluginFields) { | 307 | for (const setting of this.pluginFields) { |
295 | obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default) | 308 | const validator = (control: AbstractControl): ValidationErrors | null => { |
309 | if (!setting.commonOptions.error) return null | ||
310 | |||
311 | const error = setting.commonOptions.error({ formValues: this.form.value, value: control.value }) | ||
312 | |||
313 | return error?.error ? { [setting.commonOptions.name]: error.text } : null | ||
314 | } | ||
315 | |||
316 | const name = setting.commonOptions.name | ||
317 | |||
318 | pluginObj[name] = { | ||
319 | VALIDATORS: [ validator ], | ||
320 | MESSAGES: {} | ||
321 | } | ||
322 | |||
323 | pluginDefaults[name] = setting.commonOptions.default | ||
296 | } | 324 | } |
297 | 325 | ||
298 | this.pluginDataFormGroup = new FormGroup(obj) | 326 | this.pluginDataFormGroup = new FormGroup({}) |
327 | this.formValidatorService.updateFormGroup( | ||
328 | this.pluginDataFormGroup, | ||
329 | pluginFormErrors, | ||
330 | pluginValidationMessages, | ||
331 | pluginObj, | ||
332 | pluginDefaults | ||
333 | ) | ||
334 | |||
299 | this.form.addControl('pluginData', this.pluginDataFormGroup) | 335 | this.form.addControl('pluginData', this.pluginDataFormGroup) |
336 | this.formErrors['pluginData'] = pluginFormErrors | ||
337 | this.validationMessages['pluginData'] = pluginValidationMessages | ||
300 | 338 | ||
339 | this.cd.detectChanges() | ||
301 | this.pluginFieldsAdded.emit() | 340 | this.pluginFieldsAdded.emit() |
302 | } | 341 | } |
303 | 342 | ||
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 76f154249..fa5800897 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.form.valid || | 229 | return !this.checkForm() || |
230 | this.isUpdatingVideo === true || | 230 | this.isUpdatingVideo === true || |
231 | this.videoUploaded !== true || | 231 | this.videoUploaded !== true || |
232 | !this.videoUploadedIds.id | 232 | !this.videoUploadedIds.id |
@@ -240,7 +240,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
240 | } | 240 | } |
241 | 241 | ||
242 | updateSecondStep () { | 242 | updateSecondStep () { |
243 | if (this.isPublishingButtonDisabled() || !this.checkForm()) { | 243 | if (this.isPublishingButtonDisabled()) { |
244 | return | 244 | return |
245 | } | 245 | } |
246 | 246 | ||
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts index f2ce82360..30b59c141 100644 --- a/client/src/app/shared/shared-forms/form-reactive.ts +++ b/client/src/app/shared/shared-forms/form-reactive.ts | |||
@@ -56,13 +56,18 @@ export abstract class FormReactive { | |||
56 | 56 | ||
57 | if (control.dirty) this.formChanged = true | 57 | if (control.dirty) this.formChanged = true |
58 | 58 | ||
59 | // Don't care if dirty on force check | 59 | if (forceCheck) control.updateValueAndValidity({ emitEvent: false }) |
60 | const isDirty = control.dirty || forceCheck === true | 60 | if (!control || !control.dirty || !control.enabled || control.valid) continue |
61 | if (control && isDirty && control.enabled && !control.valid) { | 61 | |
62 | const messages = validationMessages[field] | 62 | const staticMessages = validationMessages[field] |
63 | for (const key of Object.keys(control.errors)) { | 63 | for (const key of Object.keys(control.errors)) { |
64 | formErrors[field] += messages[key] + ' ' | 64 | const formErrorValue = control.errors[key] |
65 | } | 65 | |
66 | // Try to find error message in static validation messages first | ||
67 | // Then check if the validator returns a string that is the error | ||
68 | if (typeof formErrorValue === 'boolean') formErrors[field] += staticMessages[key] + ' ' | ||
69 | else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key] | ||
70 | else throw new Error('Form error value of ' + field + ' is invalid') | ||
66 | } | 71 | } |
67 | } | 72 | } |
68 | } | 73 | } |
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 c0664de5f..055fbb2d9 100644 --- a/client/src/app/shared/shared-forms/form-validator.service.ts +++ b/client/src/app/shared/shared-forms/form-validator.service.ts | |||
@@ -40,7 +40,7 @@ export class FormValidatorService { | |||
40 | return { form, formErrors, validationMessages } | 40 | return { form, formErrors, validationMessages } |
41 | } | 41 | } |
42 | 42 | ||
43 | updateForm ( | 43 | updateFormGroup ( |
44 | form: FormGroup, | 44 | form: FormGroup, |
45 | formErrors: FormReactiveErrors, | 45 | formErrors: FormReactiveErrors, |
46 | validationMessages: FormReactiveValidationMessages, | 46 | validationMessages: FormReactiveValidationMessages, |
@@ -52,7 +52,7 @@ export class FormValidatorService { | |||
52 | 52 | ||
53 | const field = obj[name] | 53 | const field = obj[name] |
54 | if (this.isRecursiveField(field)) { | 54 | if (this.isRecursiveField(field)) { |
55 | this.updateForm( | 55 | this.updateFormGroup( |
56 | form[name], | 56 | form[name], |
57 | formErrors[name] as FormReactiveErrors, | 57 | formErrors[name] as FormReactiveErrors, |
58 | validationMessages[name] as FormReactiveValidationMessages, | 58 | validationMessages[name] as FormReactiveValidationMessages, |
@@ -66,8 +66,10 @@ export class FormValidatorService { | |||
66 | 66 | ||
67 | const defaultValue = defaultValues[name] || '' | 67 | const defaultValue = defaultValues[name] || '' |
68 | 68 | ||
69 | if (field?.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[])) | 69 | form.addControl( |
70 | else form.addControl(name, new FormControl(defaultValue)) | 70 | name, |
71 | new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[]) | ||
72 | ) | ||
71 | } | 73 | } |
72 | } | 74 | } |
73 | 75 | ||
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 2df071337..30fd63266 100644 --- a/shared/models/plugins/client/register-client-form-field.model.ts +++ b/shared/models/plugins/client/register-client-form-field.model.ts | |||
@@ -16,8 +16,15 @@ export type RegisterClientFormFieldOptions = { | |||
16 | 16 | ||
17 | // Not supported by plugin setting registration, use registerSettingsScript instead | 17 | // Not supported by plugin setting registration, use registerSettingsScript instead |
18 | hidden?: (options: any) => boolean | 18 | hidden?: (options: any) => boolean |
19 | |||
20 | // Return undefined | null if there is no error or return a string with the detailed error | ||
21 | // Not supported by plugin setting registration | ||
22 | error?: (options: any) => { error: boolean, text?: string } | ||
19 | } | 23 | } |
20 | 24 | ||
21 | export interface RegisterClientVideoFieldOptions { | 25 | export interface RegisterClientVideoFieldOptions { |
22 | type: 'update' | 'upload' | 'import-url' | 'import-torrent' | 'go-live' | 26 | type: 'update' | 'upload' | 'import-url' | 'import-torrent' | 'go-live' |
27 | |||
28 | // Default to 'plugin-settings' | ||
29 | tab?: 'main' | 'plugin-settings' | ||
23 | } | 30 | } |
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index 26fcb8987..5c1f6a2af 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -692,16 +692,31 @@ async function register ({ registerVideoField, peertubeHelpers }) { | |||
692 | type: 'input-textarea', | 692 | type: 'input-textarea', |
693 | 693 | ||
694 | default: '', | 694 | default: '', |
695 | |||
695 | // Optional, to hide a field depending on the current form state | 696 | // Optional, to hide a field depending on the current form state |
696 | // liveVideo is in the options object when the user is creating/updating a live | 697 | // liveVideo is in the options object when the user is creating/updating a live |
697 | // videoToUpdate is in the options object when the user is updating a video | 698 | // videoToUpdate is in the options object when the user is updating a video |
698 | hidden: ({ formValues, videoToUpdate, liveVideo }) => { | 699 | hidden: ({ formValues, videoToUpdate, liveVideo }) => { |
699 | return formValues.pluginData['other-field'] === 'toto' | 700 | return formValues.pluginData['other-field'] === 'toto' |
701 | }, | ||
702 | |||
703 | // Optional, to display an error depending on the form state | ||
704 | error: ({ formValues, value }) => { | ||
705 | if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false } | ||
706 | if (value === true) return { error: false } | ||
707 | |||
708 | return { error: true, text: 'Should be enabled' } | ||
700 | } | 709 | } |
701 | } | 710 | } |
702 | 711 | ||
712 | const videoFormOptions = { | ||
713 | // Optional, to choose to put your setting in a specific tab in video form | ||
714 | // type: 'main' | 'plugin-settings' | ||
715 | tab: 'main' | ||
716 | } | ||
717 | |||
703 | for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) { | 718 | for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) { |
704 | registerVideoField(commonOptions, { type }) | 719 | registerVideoField(commonOptions, { type, ...videoFormOptions }) |
705 | } | 720 | } |
706 | } | 721 | } |
707 | ``` | 722 | ``` |