1
0
Fork 0

Enhance plugin video fields

Add video form tab selection
Add ability to display an error
This commit is contained in:
Chocobozzz 2021-12-22 18:02:36 +01:00
parent 61cc1c03bf
commit 3c065fe3b3
No known key found for this signature in database
GPG key ID: 583A612D890159BE
13 changed files with 229 additions and 35 deletions

View file

@ -0,0 +1,31 @@
import { browserSleep, go } from '../utils'
export class AdminPluginPage {
async navigateToSearch () {
await go('/admin/plugins/search')
await $('my-plugin-search').waitForDisplayed()
}
async search (name: string) {
const input = $('.search-bar input')
await input.waitForDisplayed()
await input.clearValue()
await input.setValue(name)
await browserSleep(1000)
}
async installHelloWorld () {
$('.plugin-name=hello-world').waitForDisplayed()
await $('.card-body my-button[icon=cloud-download]').click()
const submitModalButton = $('.modal-content input[type=submit]')
await submitModalButton.waitForClickable()
await submitModalButton.click()
await $('.card-body my-edit-button').waitForDisplayed()
}
}

View file

@ -13,7 +13,7 @@ export class AnonymousSettingsPage {
}
async clickOnP2PCheckbox () {
const p2p = getCheckbox('p2pEnabled')
const p2p = await getCheckbox('p2pEnabled')
await p2p.waitForClickable()
await p2p.click()

View file

@ -31,7 +31,7 @@ export class MyAccountPage {
}
async clickOnP2PCheckbox () {
const p2p = getCheckbox('p2pEnabled')
const p2p = await getCheckbox('p2pEnabled')
await p2p.waitForClickable()
await p2p.scrollIntoView(false) // Avoid issues with fixed header on firefox

View file

@ -3,7 +3,10 @@ import { getCheckbox, selectCustomSelect } from '../utils'
export class VideoUploadPage {
async navigateTo () {
await $('.header .publish-button').click()
const publishButton = await $('.header .publish-button')
await publishButton.waitForClickable()
await publishButton.click()
await $('.upload-video-container').waitForDisplayed()
}
@ -24,15 +27,17 @@ export class VideoUploadPage {
// Wait for the upload to finish
await browser.waitUntil(async () => {
const actionButton = this.getSecondStepSubmitButton().$('.action-button')
const warning = await $('=Publish will be available when upload is finished').isDisplayed()
const progress = await $('.progress-bar=100%').isDisplayed()
const klass = await actionButton.getAttribute('class')
return !klass.includes('disabled')
return !warning && progress
})
}
setAsNSFW () {
return getCheckbox('nsfw').click()
async setAsNSFW () {
const checkbox = await getCheckbox('nsfw')
return checkbox.click()
}
async validSecondUploadStep (videoName: string) {
@ -51,6 +56,10 @@ export class VideoUploadPage {
return selectCustomSelect('privacy', 'Public')
}
setAsPrivate () {
return selectCustomSelect('privacy', 'Private')
}
private getSecondStepSubmitButton () {
return $('.submit-container my-button')
}

View file

@ -0,0 +1,79 @@
import { AdminPluginPage } from '../po/admin-plugin.po'
import { LoginPage } from '../po/login.po'
import { VideoUploadPage } from '../po/video-upload.po'
import { browserSleep, getCheckbox, waitServerUp } from '../utils'
describe('Plugins', () => {
let videoUploadPage: VideoUploadPage
let loginPage: LoginPage
let adminPluginPage: AdminPluginPage
function getPluginCheckbox () {
return getCheckbox('hello-world-field-4')
}
async function expectSubmitState ({ disabled }: { disabled: boolean }) {
const disabledSubmit = await $('my-button .disabled')
if (disabled) expect(await disabledSubmit.isDisplayed()).toBeTruthy()
else expect(await disabledSubmit.isDisplayed()).toBeFalsy()
}
before(async () => {
await waitServerUp()
})
beforeEach(async () => {
loginPage = new LoginPage()
videoUploadPage = new VideoUploadPage()
adminPluginPage = new AdminPluginPage()
await browser.maximizeWindow()
})
it('Should install hello world plugin', async () => {
await loginPage.loginAsRootUser()
await adminPluginPage.navigateToSearch()
await adminPluginPage.search('hello-world')
await adminPluginPage.installHelloWorld()
await browser.refresh()
})
it('Should have checkbox in video edit page', async () => {
await videoUploadPage.navigateTo()
await videoUploadPage.uploadVideo()
await $('span=Super field 4 in main tab').waitForDisplayed()
const checkbox = await getPluginCheckbox()
expect(await checkbox.isDisplayed()).toBeTruthy()
await expectSubmitState({ disabled: true })
})
it('Should check the checkbox and be able to submit the video', async function () {
const checkbox = await getPluginCheckbox()
await checkbox.click()
await expectSubmitState({ disabled: false })
})
it('Should uncheck the checkbox and not be able to submit the video', async function () {
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()
})
it('Should change the privacy and should hide the checkbox', async function () {
await videoUploadPage.setAsPrivate()
await expectSubmitState({ disabled: false })
})
})

View file

@ -1,5 +1,5 @@
function getCheckbox (name: string) {
return $(`my-peertube-checkbox[inputname=${name}] label`)
return $(`my-peertube-checkbox input[id=${name}]`).parentElement()
}
async function selectCustomSelect (id: string, valueLabel: string) {

View file

@ -146,6 +146,13 @@
</ng-template>
</my-peertube-checkbox>
<ng-container ngbNavItem *ngIf="getPluginsFields('main').length !== 0">
<div *ngFor="let pluginSetting of getPluginsFields('main')" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)">
<my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors['pluginData']" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
</div>
</ng-container>
</div>
</div>
</ng-template>
@ -339,15 +346,15 @@
</ng-template>
</ng-container>
<ng-container ngbNavItem *ngIf="pluginFields.length !== 0">
<ng-container ngbNavItem *ngIf="getPluginsFields('plugin-settings').length !== 0">
<a ngbNavLink i18n>Plugin settings</a>
<ng-template ngbNavContent>
<div class="row plugin-settings">
<div class="col-md-12 col-xl-8">
<div *ngFor="let pluginSetting of pluginFields" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)">
<my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
<div *ngFor="let pluginSetting of getPluginsFields('plugin-settings')" class="form-group" [hidden]="isPluginFieldHidden(pluginSetting)">
<my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors['pluginData']" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
</div>
</div>

View file

@ -1,10 +1,11 @@
import { forkJoin } from 'rxjs'
import { map } from 'rxjs/operators'
import { SelectChannelItem } from 'src/types/select-options-item.model'
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'
import { HooksService, PluginService, ServerService } from '@app/core'
import { removeElementFromArray } from '@app/helpers'
import { BuildFormValidator } from '@app/shared/form-validators'
import {
VIDEO_CATEGORY_VALIDATOR,
VIDEO_CHANNEL_VALIDATOR,
@ -101,7 +102,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
private instanceService: InstanceService,
private i18nPrimengCalendarService: I18nPrimengCalendarService,
private ngZone: NgZone,
private hooks: HooksService
private hooks: HooksService,
private cd: ChangeDetectorRef
) {
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
@ -116,7 +118,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
licence: this.serverConfig.defaults.publish.licence,
tags: []
}
const obj: any = {
const obj: { [ id: string ]: BuildFormValidator } = {
name: VIDEO_NAME_VALIDATOR,
privacy: VIDEO_PRIVACY_VALIDATOR,
channelId: VIDEO_CHANNEL_VALIDATOR,
@ -138,7 +140,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
saveReplay: null
}
this.formValidatorService.updateForm(
this.formValidatorService.updateFormGroup(
this.form,
this.formErrors,
this.validationMessages,
@ -275,6 +277,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
})
}
getPluginsFields (tab: 'main' | 'plugin-settings') {
return this.pluginFields.filter(p => {
const wanted = p.videoFormOptions.tab ?? 'plugin-settings'
return wanted === tab
})
}
private sortVideoCaptions () {
this.videoCaptions.sort((v1, v2) => {
if (v1.language.label < v2.language.label) return -1
@ -289,15 +299,44 @@ export class VideoEditComponent implements OnInit, OnDestroy {
if (this.pluginFields.length === 0) return
const obj: any = {}
const pluginObj: { [ id: string ]: BuildFormValidator } = {}
const pluginValidationMessages: FormReactiveValidationMessages = {}
const pluginFormErrors: any = {}
const pluginDefaults: any = {}
for (const setting of this.pluginFields) {
obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default)
const validator = (control: AbstractControl): ValidationErrors | null => {
if (!setting.commonOptions.error) return null
const error = setting.commonOptions.error({ formValues: this.form.value, value: control.value })
return error?.error ? { [setting.commonOptions.name]: error.text } : null
}
this.pluginDataFormGroup = new FormGroup(obj)
this.form.addControl('pluginData', this.pluginDataFormGroup)
const name = setting.commonOptions.name
pluginObj[name] = {
VALIDATORS: [ validator ],
MESSAGES: {}
}
pluginDefaults[name] = setting.commonOptions.default
}
this.pluginDataFormGroup = new FormGroup({})
this.formValidatorService.updateFormGroup(
this.pluginDataFormGroup,
pluginFormErrors,
pluginValidationMessages,
pluginObj,
pluginDefaults
)
this.form.addControl('pluginData', this.pluginDataFormGroup)
this.formErrors['pluginData'] = pluginFormErrors
this.validationMessages['pluginData'] = pluginValidationMessages
this.cd.detectChanges()
this.pluginFieldsAdded.emit()
}

View file

@ -226,7 +226,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
}
isPublishingButtonDisabled () {
return !this.form.valid ||
return !this.checkForm() ||
this.isUpdatingVideo === true ||
this.videoUploaded !== true ||
!this.videoUploadedIds.id
@ -240,7 +240,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
}
updateSecondStep () {
if (this.isPublishingButtonDisabled() || !this.checkForm()) {
if (this.isPublishingButtonDisabled()) {
return
}

View file

@ -56,13 +56,18 @@ export abstract class FormReactive {
if (control.dirty) this.formChanged = true
// Don't care if dirty on force check
const isDirty = control.dirty || forceCheck === true
if (control && isDirty && control.enabled && !control.valid) {
const messages = validationMessages[field]
if (forceCheck) control.updateValueAndValidity({ emitEvent: false })
if (!control || !control.dirty || !control.enabled || control.valid) continue
const staticMessages = validationMessages[field]
for (const key of Object.keys(control.errors)) {
formErrors[field] += messages[key] + ' '
}
const formErrorValue = control.errors[key]
// 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] + ' '
else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
else throw new Error('Form error value of ' + field + ' is invalid')
}
}
}

View file

@ -40,7 +40,7 @@ export class FormValidatorService {
return { form, formErrors, validationMessages }
}
updateForm (
updateFormGroup (
form: FormGroup,
formErrors: FormReactiveErrors,
validationMessages: FormReactiveValidationMessages,
@ -52,7 +52,7 @@ export class FormValidatorService {
const field = obj[name]
if (this.isRecursiveField(field)) {
this.updateForm(
this.updateFormGroup(
form[name],
formErrors[name] as FormReactiveErrors,
validationMessages[name] as FormReactiveValidationMessages,
@ -66,8 +66,10 @@ export class FormValidatorService {
const defaultValue = defaultValues[name] || ''
if (field?.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[]))
else form.addControl(name, new FormControl(defaultValue))
form.addControl(
name,
new FormControl(defaultValue, field?.VALIDATORS as ValidatorFn[])
)
}
}

View file

@ -16,8 +16,15 @@ export type RegisterClientFormFieldOptions = {
// Not supported by plugin setting registration, use registerSettingsScript instead
hidden?: (options: any) => boolean
// 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 }
}
export interface RegisterClientVideoFieldOptions {
type: 'update' | 'upload' | 'import-url' | 'import-torrent' | 'go-live'
// Default to 'plugin-settings'
tab?: 'main' | 'plugin-settings'
}

View file

@ -692,16 +692,31 @@ async function register ({ registerVideoField, peertubeHelpers }) {
type: 'input-textarea',
default: '',
// Optional, to hide a field depending on the current form state
// liveVideo is in the options object when the user is creating/updating a live
// videoToUpdate is in the options object when the user is updating a video
hidden: ({ formValues, videoToUpdate, liveVideo }) => {
return formValues.pluginData['other-field'] === 'toto'
},
// Optional, to display an error depending on the form state
error: ({ formValues, value }) => {
if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
if (value === true) return { error: false }
return { error: true, text: 'Should be enabled' }
}
}
const videoFormOptions = {
// Optional, to choose to put your setting in a specific tab in video form
// type: 'main' | 'plugin-settings'
tab: 'main'
}
for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
registerVideoField(commonOptions, { type })
registerVideoField(commonOptions, { type, ...videoFormOptions })
}
}
```