Enhance plugin video fields
Add video form tab selection Add ability to display an error
This commit is contained in:
parent
61cc1c03bf
commit
3c065fe3b3
13 changed files with 229 additions and 35 deletions
31
client/e2e/src/po/admin-plugin.po.ts
Normal file
31
client/e2e/src/po/admin-plugin.po.ts
Normal 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()
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ export class AnonymousSettingsPage {
|
|||
}
|
||||
|
||||
async clickOnP2PCheckbox () {
|
||||
const p2p = getCheckbox('p2pEnabled')
|
||||
const p2p = await getCheckbox('p2pEnabled')
|
||||
await p2p.waitForClickable()
|
||||
|
||||
await p2p.click()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
79
client/e2e/src/suites-local/plugins.e2e-spec.ts
Normal file
79
client/e2e/src/suites-local/plugins.e2e-spec.ts
Normal 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 })
|
||||
})
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
Loading…
Reference in a new issue