aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-12-22 18:02:36 +0100
committerChocobozzz <me@florianbigard.com>2021-12-29 10:10:01 +0100
commit3c065fe3b3e1385d59ad1980251d14b712648155 (patch)
treef5f7f1b428b8155a735014304e2d45b9ed92fe07 /client
parent61cc1c03bf6f12af7c1b2e2a7d2fdaa563c37b59 (diff)
downloadPeerTube-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
Diffstat (limited to 'client')
-rw-r--r--client/e2e/src/po/admin-plugin.po.ts31
-rw-r--r--client/e2e/src/po/anonymous-settings.po.ts2
-rw-r--r--client/e2e/src/po/my-account.po.ts2
-rw-r--r--client/e2e/src/po/video-upload.po.ts21
-rw-r--r--client/e2e/src/suites-local/plugins.e2e-spec.ts79
-rw-r--r--client/e2e/src/utils/elements.ts2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html13
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts55
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts4
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.ts19
-rw-r--r--client/src/app/shared/shared-forms/form-validator.service.ts10
11 files changed, 205 insertions, 33 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 @@
1import { browserSleep, go } from '../utils'
2
3export 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
4export class VideoUploadPage { 4export 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 @@
1import { AdminPluginPage } from '../po/admin-plugin.po'
2import { LoginPage } from '../po/login.po'
3import { VideoUploadPage } from '../po/video-upload.po'
4import { browserSleep, getCheckbox, waitServerUp } from '../utils'
5
6describe('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 @@
1function getCheckbox (name: string) { 1function getCheckbox (name: string) {
2 return $(`my-peertube-checkbox[inputname=${name}] label`) 2 return $(`my-peertube-checkbox input[id=${name}]`).parentElement()
3} 3}
4 4
5async function selectCustomSelect (id: string, valueLabel: string) { 5async 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 @@
1import { forkJoin } from 'rxjs' 1import { 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 { 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 { FormArray, FormControl, FormGroup, Validators } from '@angular/forms' 5import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, 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 { 9import {
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