<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
+ <div class="inner-form-title">Instance</div>
+
+ <div class="form-group">
+ <label for="instanceName">Name</label>
+ <input
+ type="text" id="instanceName"
+ formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
+ >
+ <div *ngIf="formErrors.instanceName" class="form-error">
+ {{ formErrors.instanceName }}
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="instanceDescription">Description (markdown)</label>
+ <my-markdown-textarea
+ id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
+ [classes]="{ 'input-error': formErrors['instanceDescription'] }"
+ ></my-markdown-textarea>
+ <div *ngIf="formErrors.instanceDescription" class="form-error">
+ {{ formErrors.instanceDescription }}
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="instanceTerms">Terms (markdown)</label>
+ <my-markdown-textarea
+ id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
+ [ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
+ ></my-markdown-textarea>
+ <div *ngIf="formErrors.instanceTerms" class="form-error">
+ {{ formErrors.instanceTerms }}
+ </div>
+ </div>
+
<div class="inner-form-title">Cache</div>
<div class="form-group">
import { ConfigService } from '@app/+admin/config/shared/config.service'
import { ServerService } from '@app/core/server/server.service'
import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
-import { ADMIN_EMAIL, CACHE_PREVIEWS_SIZE, SIGNUP_LIMIT, TRANSCODING_THREADS } from '@app/shared/forms/form-validators/custom-config'
+import {
+ ADMIN_EMAIL,
+ CACHE_PREVIEWS_SIZE,
+ INSTANCE_NAME,
+ SIGNUP_LIMIT,
+ TRANSCODING_THREADS
+} from '@app/shared/forms/form-validators/custom-config'
import { NotificationsService } from 'angular2-notifications'
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
form: FormGroup
formErrors = {
+ instanceName: '',
+ instanceDescription: '',
+ instanceTerms: '',
cachePreviewsSize: '',
signupLimit: '',
adminEmail: '',
transcodingThreads: ''
}
validationMessages = {
+ instanceName: INSTANCE_NAME.MESSAGES,
cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
signupLimit: SIGNUP_LIMIT.MESSAGES,
adminEmail: ADMIN_EMAIL.MESSAGES,
buildForm () {
const formGroupData = {
+ instanceName: [ '', INSTANCE_NAME.VALIDATORS ],
+ instanceDescription: [ '' ],
+ instanceTerms: [ '' ],
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
signupEnabled: [ ],
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
formValidated () {
const data = {
+ instance: {
+ name: this.form.value['instanceName'],
+ description: this.form.value['instanceDescription'],
+ terms: this.form.value['instanceTerms']
+ },
cache: {
previews: {
size: this.form.value['cachePreviewsSize']
this.serverService.loadConfig()
this.updateForm()
+
+ this.notificationsService.success('Success', 'Configuration updated.')
},
err => this.notificationsService.error('Error', err.message)
private updateForm () {
const data = {
+ instanceName: this.customConfig.instance.name,
+ instanceDescription: this.customConfig.instance.description,
+ instanceTerms: this.customConfig.instance.terms,
cachePreviewsSize: this.customConfig.cache.previews.size,
signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit,
import { Validators } from '@angular/forms'
+export const INSTANCE_NAME = {
+ VALIDATORS: [ Validators.required ],
+ MESSAGES: {
+ 'required': 'Instance name is required.',
+ }
+}
+
export const CACHE_PREVIEWS_SIZE = {
VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
MESSAGES: {
--- /dev/null
+<div class="root" [ngStyle]="{ 'flex-direction': flexDirection }">
+ <textarea
+ [(ngModel)]="description" (ngModelChange)="onModelChange()"
+ [ngClass]="classes" [ngStyle]="{ width: textareaWidth, height: textareaHeight, 'margin-right': textareaMarginRight }"
+ id="description" name="description">
+ </textarea>
+
+ <tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews">
+ <tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
+ <tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
+ </tabset>
+</div>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.root {
+ display: flex;
+
+ textarea {
+ @include peertube-textarea(100%, 150px);
+
+ margin-bottom: 15px;
+ }
+
+ /deep/ {
+ .nav-link {
+ display: flex !important;
+ align-items: center;
+ height: 30px !important;
+ padding: 0 15px !important;
+ }
+
+ .tab-content {
+ min-height: 75px;
+ padding: 15px;
+ font-size: 15px;
+ }
+ }
+}
--- /dev/null
+import { Component, forwardRef, Input, OnInit } from '@angular/core'
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
+import 'rxjs/add/operator/debounceTime'
+import 'rxjs/add/operator/distinctUntilChanged'
+import { isInMobileView } from '@app/shared/misc/utils'
+import { MarkdownService } from '@app/videos/shared'
+import { Subject } from 'rxjs/Subject'
+import truncate from 'lodash-es/truncate'
+
+@Component({
+ selector: 'my-markdown-textarea',
+ templateUrl: './markdown-textarea.component.html',
+ styleUrls: [ './markdown-textarea.component.scss' ],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MarkdownTextareaComponent),
+ multi: true
+ }
+ ]
+})
+
+export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
+ @Input() description = ''
+ @Input() classes: string[] = []
+ @Input() textareaWidth = '100%'
+ @Input() textareaHeight = '150px'
+ @Input() previewColumn = false
+ @Input() truncate: number
+
+ textareaMarginRight = '0'
+ flexDirection = 'column'
+ truncatedDescriptionHTML = ''
+ descriptionHTML = ''
+
+ private descriptionChanged = new Subject<string>()
+
+ constructor (private markdownService: MarkdownService) {}
+
+ ngOnInit () {
+ this.descriptionChanged
+ .debounceTime(150)
+ .distinctUntilChanged()
+ .subscribe(() => this.updateDescriptionPreviews())
+
+ this.descriptionChanged.next(this.description)
+
+ if (this.previewColumn) {
+ this.flexDirection = 'row'
+ this.textareaMarginRight = '15px'
+ }
+ }
+
+ propagateChange = (_: any) => { /* empty */ }
+
+ writeValue (description: string) {
+ this.description = description
+
+ this.descriptionChanged.next(this.description)
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ onModelChange () {
+ this.propagateChange(this.description)
+
+ this.descriptionChanged.next(this.description)
+ }
+
+ arePreviewsDisplayed () {
+ return isInMobileView() === false
+ }
+
+ private updateDescriptionPreviews () {
+ if (this.description === null || this.description === undefined) return
+
+ this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: this.truncate }))
+ this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
+ }
+}
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
+import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
+import { MarkdownService } from '@app/videos/shared'
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
import { ModalModule } from 'ngx-bootstrap/modal'
+import { TabsModule } from 'ngx-bootstrap/tabs'
import { InfiniteScrollModule } from 'ngx-infinite-scroll'
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
PrimeSharedModule,
InfiniteScrollModule,
- NgPipesModule
+ NgPipesModule,
+ TabsModule.forRoot()
],
declarations: [
DeleteButtonComponent,
EditButtonComponent,
NumberFormatterPipe,
- FromNowPipe
+ FromNowPipe,
+ MarkdownTextareaComponent
],
exports: [
VideoMiniatureComponent,
DeleteButtonComponent,
EditButtonComponent,
+ MarkdownTextareaComponent,
NumberFormatterPipe,
FromNowPipe
VideoAbuseService,
VideoBlacklistService,
UserService,
- VideoService
+ VideoService,
+ MarkdownService
]
})
export class SharedModule { }
}
updateVideo (video: VideoEdit) {
- const language = video.language || undefined
- const licence = video.licence || undefined
- const category = video.category || undefined
- const description = video.description || undefined
+ const language = video.language || null
+ const licence = video.licence || null
+ const category = video.category || null
+ const description = video.description || null
const body: VideoUpdate = {
name: video.name,
+++ /dev/null
-<textarea
- [(ngModel)]="description" (ngModelChange)="onModelChange()"
- id="description" name="description">
-</textarea>
-
-<tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews">
- <tab heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
- <tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
-</tabset>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-textarea {
- @include peertube-textarea(100%, 150px);
-
- margin-bottom: 15px;
-}
-
-/deep/ {
- .nav-link {
- display: flex !important;
- align-items: center;
- height: 30px !important;
- padding: 0 15px !important;
- }
-
- .tab-content {
- min-height: 75px;
- padding: 15px;
- font-size: 15px;
- }
-}
-
+++ /dev/null
-import { Component, forwardRef, Input, OnInit } from '@angular/core'
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
-import 'rxjs/add/operator/debounceTime'
-import 'rxjs/add/operator/distinctUntilChanged'
-import { isInMobileView } from '@app/shared/misc/utils'
-import { Subject } from 'rxjs/Subject'
-import { MarkdownService } from '../../shared'
-import truncate from 'lodash-es/truncate'
-
-@Component({
- selector: 'my-video-description',
- templateUrl: './video-description.component.html',
- styleUrls: [ './video-description.component.scss' ],
- providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => VideoDescriptionComponent),
- multi: true
- }
- ]
-})
-
-export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
- @Input() description = ''
- truncatedDescriptionHTML = ''
- descriptionHTML = ''
-
- private descriptionChanged = new Subject<string>()
-
- constructor (private markdownService: MarkdownService) {}
-
- ngOnInit () {
- this.descriptionChanged
- .debounceTime(150)
- .distinctUntilChanged()
- .subscribe(() => this.updateDescriptionPreviews())
-
- this.descriptionChanged.next(this.description)
- }
-
- propagateChange = (_: any) => { /* empty */ }
-
- writeValue (description: string) {
- this.description = description
-
- this.descriptionChanged.next(this.description)
- }
-
- registerOnChange (fn: (_: any) => void) {
- this.propagateChange = fn
- }
-
- registerOnTouched () {
- // Unused
- }
-
- onModelChange () {
- this.propagateChange(this.description)
-
- this.descriptionChanged.next(this.description)
- }
-
- arePreviewsDisplayed () {
- return isInMobileView() === false
- }
-
- private updateDescriptionPreviews () {
- if (!this.description) return
-
- this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
- this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
- }
-}
<div class="form-group">
<label class="label-tags">Tags</label> <span>(press Enter to add)</span>
<tag-input
- [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
- formControlName="tags" maxItems="5" modelAsStrings="true"
+ [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+ formControlName="tags" maxItems="5" modelAsStrings="true"
></tag-input>
</div>
<div class="form-group">
<label for="description">Description</label>
- <my-video-description formControlName="description"></my-video-description>
+ <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
<div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
import { NgModule } from '@angular/core'
-
-import { TagInputModule } from 'ngx-chips'
import { TabsModule } from 'ngx-bootstrap/tabs'
-
-import { MarkdownService } from '../../shared'
+import { TagInputModule } from 'ngx-chips'
import { SharedModule } from '../../../shared'
-import { VideoDescriptionComponent } from './video-description.component'
import { VideoEditComponent } from './video-edit.component'
@NgModule({
imports: [
TagInputModule,
- TabsModule.forRoot(),
SharedModule
],
declarations: [
- VideoDescriptionComponent,
VideoEditComponent
],
TagInputModule,
TabsModule,
- VideoDescriptionComponent,
VideoEditComponent
],
- providers: [
- MarkdownService
- ]
+ providers: []
})
export class VideoEditModule { }
.enable('link')
.enable('newline')
+ this.setTargetToLinks()
+ }
+
+ markdownToHTML (markdown: string) {
+ const html = this.markdownIt.render(markdown)
+
+ // Avoid linkify truncated links
+ return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...')
+ }
+
+ private setTargetToLinks () {
// Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
const defaultRender = this.markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options)
return defaultRender(tokens, idx, options, env, self)
}
}
-
- markdownToHTML (markdown: string) {
- const html = this.markdownIt.render(markdown)
-
- // Avoid linkify truncated links
- return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...')
- }
}
480p: true
720p: true
1080p: true
+
+instance:
+ name: 'PeerTube'
+ description: '' # Support markdown
+ terms: '' # Support markdown
480p: true
720p: true
1080p: true
+
+instance:
+ name: 'PeerTube'
+ description: '' # Support markdown
+ terms: '' # Support markdown
function customConfig (): CustomConfig {
return {
+ instance: {
+ name: CONFIG.INSTANCE.NAME,
+ description: CONFIG.INSTANCE.DESCRIPTION,
+ terms: CONFIG.INSTANCE.TERMS
+ },
cache: {
previews: {
size: CONFIG.CACHE.PREVIEWS.SIZE
'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password',
'storage.videos', 'storage.logs', 'storage.thumbnails', 'storage.previews', 'storage.torrents', 'storage.cache', 'log.level',
'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
- 'user.video_quota', 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address'
+ 'user.video_quota', 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
+ 'instance.name', 'instance.description', 'instance.terms'
]
const miss: string[] = []
PREVIEWS: {
get SIZE () { return config.get<number>('cache.previews.size') }
}
+ },
+ INSTANCE: {
+ get NAME () { return config.get<string>('instance.name') },
+ get DESCRIPTION () { return config.get<string>('instance.description') },
+ get TERMS () { return config.get<string>('instance.terms') }
}
}
let server: ServerInfo
let userAccessToken: string
const updateParams: CustomConfig = {
+ instance: {
+ name: 'PeerTube updated',
+ description: 'my super description',
+ terms: 'my super terms'
+ },
cache: {
previews: {
size: 2
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
+ expect(data.instance.name).to.equal('PeerTube')
+ expect(data.instance.description).to.be.empty
+ expect(data.instance.terms).to.be.empty
expect(data.cache.previews.size).to.equal(1)
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
it('Should update the customized configuration', async function () {
const newCustomConfig = {
+ instance: {
+ name: 'PeerTube updated',
+ description: 'my super description',
+ terms: 'my super terms'
+ },
cache: {
previews: {
size: 2
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
- expect(data.cache.previews.size).to.equal(2)
+ expect(data.instance.name).to.equal('PeerTube updated')
+ expect(data.instance.description).to.equal('my super description')
+ expect(data.instance.terms).to.equal('my super terms')
expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5)
expect(data.admin.email).to.equal('superadmin1@example.com')
expect(dateIsValid(video.createdAt)).to.be.true
expect(dateIsValid(video.updatedAt)).to.be.true
- const res = await getVideo(url, video.id)
+ const res = await getVideo(url, video.uuid)
const videoDetails = res.body
expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
export interface CustomConfig {
+ instance: {
+ name: string
+ description: string
+ terms: string
+ }
+
cache: {
previews: {
size: number