-
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 @@
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
+ }
+
+ const name = setting.commonOptions.name
+
+ pluginObj[name] = {
+ VALIDATORS: [ validator ],
+ MESSAGES: {}
+ }
+
+ pluginDefaults[name] = setting.commonOptions.default
}
- this.pluginDataFormGroup = new FormGroup(obj)
- this.form.addControl('pluginData', this.pluginDataFormGroup)
+ 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()
}
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
}
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
}
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 {
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]
- for (const key of Object.keys(control.errors)) {
- formErrors[field] += messages[key] + ' '
- }
+ 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)) {
+ 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')
}
}
}
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 {
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[])
+ )
}
}
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 = {
// 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'
}
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 }) {
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 })
}
}
```