Add new name/terms/description config options

pull/277/head
Chocobozzz 2018-01-31 16:42:40 +01:00
parent 81ebea48bf
commit 66b16cafb3
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
22 changed files with 201 additions and 73 deletions

View File

@ -2,6 +2,41 @@
<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">

View File

@ -4,7 +4,13 @@ import { Router } from '@angular/router'
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'
@ -36,6 +42,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
form: FormGroup
formErrors = {
instanceName: '',
instanceDescription: '',
instanceTerms: '',
cachePreviewsSize: '',
signupLimit: '',
adminEmail: '',
@ -43,6 +52,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
transcodingThreads: ''
}
validationMessages = {
instanceName: INSTANCE_NAME.MESSAGES,
cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
signupLimit: SIGNUP_LIMIT.MESSAGES,
adminEmail: ADMIN_EMAIL.MESSAGES,
@ -65,6 +75,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
buildForm () {
const formGroupData = {
instanceName: [ '', INSTANCE_NAME.VALIDATORS ],
instanceDescription: [ '' ],
instanceTerms: [ '' ],
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
signupEnabled: [ ],
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
@ -109,6 +122,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
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']
@ -146,6 +164,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
this.serverService.loadConfig()
this.updateForm()
this.notificationsService.success('Success', 'Configuration updated.')
},
err => this.notificationsService.error('Error', err.message)
@ -154,6 +174,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
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,

View File

@ -1,5 +1,12 @@
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: {

View File

@ -0,0 +1,12 @@
<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>

View File

@ -0,0 +1,27 @@
@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;
}
}
}

View File

@ -3,25 +3,33 @@ 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 { MarkdownService } from '../../shared'
import truncate from 'lodash-es/truncate'
@Component({
selector: 'my-video-description',
templateUrl: './video-description.component.html',
styleUrls: [ './video-description.component.scss' ],
selector: 'my-markdown-textarea',
templateUrl: './markdown-textarea.component.html',
styleUrls: [ './markdown-textarea.component.scss' ],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => VideoDescriptionComponent),
useExisting: forwardRef(() => MarkdownTextareaComponent),
multi: true
}
]
})
export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
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 = ''
@ -36,6 +44,11 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
.subscribe(() => this.updateDescriptionPreviews())
this.descriptionChanged.next(this.description)
if (this.previewColumn) {
this.flexDirection = 'row'
this.textareaMarginRight = '15px'
}
}
propagateChange = (_: any) => { /* empty */ }
@ -65,9 +78,9 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
}
private updateDescriptionPreviews () {
if (!this.description) return
if (this.description === null || this.description === undefined) return
this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: this.truncate }))
this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
}
}

View File

@ -3,10 +3,13 @@ import { HttpClientModule } from '@angular/common/http'
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'
@ -40,7 +43,8 @@ import { VideoService } from './video/video.service'
PrimeSharedModule,
InfiniteScrollModule,
NgPipesModule
NgPipesModule,
TabsModule.forRoot()
],
declarations: [
@ -50,7 +54,8 @@ import { VideoService } from './video/video.service'
DeleteButtonComponent,
EditButtonComponent,
NumberFormatterPipe,
FromNowPipe
FromNowPipe,
MarkdownTextareaComponent
],
exports: [
@ -74,6 +79,7 @@ import { VideoService } from './video/video.service'
VideoMiniatureComponent,
DeleteButtonComponent,
EditButtonComponent,
MarkdownTextareaComponent,
NumberFormatterPipe,
FromNowPipe
@ -86,7 +92,8 @@ import { VideoService } from './video/video.service'
VideoAbuseService,
VideoBlacklistService,
UserService,
VideoService
VideoService,
MarkdownService
]
})
export class SharedModule { }

View File

@ -42,10 +42,10 @@ export class VideoService {
}
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,

View File

@ -1,9 +0,0 @@
<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>

View File

@ -1,24 +0,0 @@
@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;
}
}

View File

@ -12,14 +12,14 @@
<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 }}

View File

@ -1,23 +1,17 @@
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
],
@ -25,12 +19,9 @@ import { VideoEditComponent } from './video-edit.component'
TagInputModule,
TabsModule,
VideoDescriptionComponent,
VideoEditComponent
],
providers: [
MarkdownService
]
providers: []
})
export class VideoEditModule { }

View File

@ -14,6 +14,17 @@ export class MarkdownService {
.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)
@ -33,11 +44,4 @@ export class MarkdownService {
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...')
}
}

View File

@ -69,3 +69,8 @@ transcoding:
480p: true
720p: true
1080p: true
instance:
name: 'PeerTube'
description: '' # Support markdown
terms: '' # Support markdown

View File

@ -69,3 +69,8 @@ transcoding:
480p: true
720p: true
1080p: true
instance:
name: 'PeerTube'
description: '' # Support markdown
terms: '' # Support markdown

View File

@ -105,6 +105,11 @@ export {
function customConfig (): CustomConfig {
return {
instance: {
name: CONFIG.INSTANCE.NAME,
description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS
},
cache: {
previews: {
size: CONFIG.CACHE.PREVIEWS.SIZE

View File

@ -23,7 +23,8 @@ function checkMissedConfig () {
'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[] = []

View File

@ -154,6 +154,11 @@ const CONFIG = {
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') }
}
}

View File

@ -14,6 +14,11 @@ describe('Test config API validators', function () {
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

View File

@ -49,6 +49,9 @@ describe('Test config', function () {
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)
@ -65,6 +68,11 @@ describe('Test config', function () {
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
@ -97,7 +105,9 @@ describe('Test config', function () {
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')

View File

@ -373,7 +373,7 @@ async function completeVideoCheck (
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)

View File

@ -1,4 +1,10 @@
export interface CustomConfig {
instance: {
name: string
description: string
terms: string
}
cache: {
previews: {
size: number