Add video channel management
authorChocobozzz <me@florianbigard.com>
Thu, 26 Apr 2018 14:11:38 +0000 (16:11 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 26 Apr 2018 14:18:01 +0000 (16:18 +0200)
36 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/follows/follows.component.html
client/src/app/+admin/follows/follows.component.scss
client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
client/src/app/+admin/users/user-edit/user-edit.component.html
client/src/app/+admin/users/user-edit/user-edit.component.scss
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.scss
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
client/src/app/my-account/my-account-routing.module.ts
client/src/app/my-account/my-account-video-channels/my-account-video-channel-create.component.ts [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.html [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.scss [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.ts [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channel-update.component.ts [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.html [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.scss [new file with mode: 0644]
client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.ts [new file with mode: 0644]
client/src/app/my-account/my-account-videos/my-account-videos.component.ts
client/src/app/my-account/my-account.component.html
client/src/app/my-account/my-account.module.ts
client/src/app/shared/forms/form-validators/video-channel.ts [new file with mode: 0644]
client/src/app/shared/video-channel/video-channel.service.ts
client/src/sass/application.scss
client/src/sass/include/_mixins.scss
server/controllers/api/video-channel.ts
server/lib/user.ts
server/lib/video-channel.ts
server/middlewares/validators/video-channels.ts
server/tests/api/check-params/video-channels.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/video-channels.ts
server/tests/utils/videos/video-channels.ts
shared/models/videos/video-channel-create.model.ts
shared/models/videos/video-channel-update.model.ts

index df40bba9f61ba6aae46e87ef0009dc0026803f65..02125245633067ad6073c62187c42d3f4f98fa96 100644 (file)
@@ -1,4 +1,4 @@
-<div class="admin-sub-title">Update PeerTube configuration</div>
+<div class="form-sub-title">Update PeerTube configuration</div>
 
 <form role="form" [formGroup]="form">
 
index d3d7486229f4eca5cd05d315e38aecabb474a938..71e82059cf6151a8276dc96f17db0eb4f9d1595f 100644 (file)
@@ -1,5 +1,5 @@
 <div class="admin-sub-header">
-  <div class="admin-sub-title">Manage follows</div>
+  <div class="form-sub-title">Manage follows</div>
 
   <tabset #followsMenuTabs>
     <tab *ngFor="let link of links">
index 385dfbb7d212dd002c7f4e5b068bfc2a26ec0e9a..08b3737f838ec1b5b9207b9b84b1898a73797b5b 100644 (file)
@@ -1,4 +1,4 @@
-.admin-sub-title {
+.form-sub-title {
   flex-grow: 0;
   margin-right: 30px;
 }
index 2bc8ab3dd207b66748600f058c76fc7f43de65fc..0cc4dc3e0378cd75e5c2945916174907e57c1703 100644 (file)
@@ -1,5 +1,5 @@
 <div class="admin-sub-header">
-  <div class="admin-sub-title">Jobs list</div>
+  <div class="form-sub-title">Jobs list</div>
 
   <div class="peertube-select-container">
     <select [(ngModel)]="jobState" (ngModelChange)="onJobStateChanged()">
index a68dd231badfe2ef9ec43ebf97590f39fa8b92b0..6dee0b7aa5cf605cf920a1508b27d21055ba42bb 100644 (file)
@@ -1,5 +1,5 @@
-<div class="admin-sub-title" *ngIf="isCreation() === true">Add user</div>
-<div class="admin-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
+<div class="form-sub-title" *ngIf="isCreation() === true">Add user</div>
+<div class="form-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
 
 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
 
index 59bb8e3e49f9b64e9fda4b8893309ab9a122890a..b74604e3b55020b8107acb95b828de9be3146fd0 100644 (file)
@@ -1,7 +1,7 @@
 @import '_variables';
 @import '_mixins';
 
-.admin-sub-title {
+.form-sub-title {
   margin-bottom: 30px;
 }
 
index 8dbe9ddc4e70b0fa4021e3edd28c649a36e7807c..83db034e3374613c35e848413e5336c579ad9d76 100644 (file)
@@ -1,5 +1,5 @@
 <div class="admin-sub-header">
-  <div class="admin-sub-title">Users list</div>
+  <div class="form-sub-title">Users list</div>
 
   <a class="add-button" routerLink="/admin/users/add">
     <span class="icon icon-add"></span>
index 72d31a0ccbda5eeba3e26b0dd6a94b61e80927da..4a66b5d8dd7969f85f9bf4017f478aa8a06b4ba7 100644 (file)
@@ -2,13 +2,5 @@
 @import '_mixins';
 
 .add-button {
-  @include peertube-button-link;
-  @include orange-button;
-
-  .icon.icon-add {
-    @include icon(22px);
-
-    margin-right: 3px;
-    background-image: url('../../../../assets/images/admin/add.svg');
-  }
+  @include create-button;
 }
index 13a5b1117d510ee9a2349e304da509f42964b56e..5f70db991afb358823bb94a4845153fd8a63ebb1 100644 (file)
@@ -1,5 +1,5 @@
 <div class="admin-sub-header">
-  <div class="admin-sub-title">Video abuses list</div>
+  <div class="form-sub-title">Video abuses list</div>
 </div>
 
 <p-table
index ac30cec3984e38a1e0d18d6b4d019d437d1e12f5..0a0fcb762bcc074d540e1d69a0002545fc111a16 100644 (file)
@@ -1,5 +1,5 @@
 <div class="admin-sub-header">
-  <div class="admin-sub-title">Blacklisted videos</div>
+  <div class="form-sub-title">Blacklisted videos</div>
 </div>
 
 <p-table
index 5a61db41fef81b75b2fd7c86ae1a134f82e1602b..96f52c1da2c8e2ba69d1954e0a92cecad570f693 100644 (file)
@@ -5,6 +5,9 @@ import { LoginGuard } from '../core'
 import { MyAccountComponent } from './my-account.component'
 import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
 import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
+import { MyAccountVideoChannelsComponent } from '@app/my-account/my-account-video-channels/my-account-video-channels.component'
+import { MyAccountVideoChannelCreateComponent } from '@app/my-account/my-account-video-channels/my-account-video-channel-create.component'
+import { MyAccountVideoChannelUpdateComponent } from '@app/my-account/my-account-video-channels/my-account-video-channel-update.component'
 
 const myAccountRoutes: Routes = [
   {
@@ -21,6 +24,33 @@ const myAccountRoutes: Routes = [
           }
         }
       },
+      {
+        path: 'video-channels',
+        component: MyAccountVideoChannelsComponent,
+        data: {
+          meta: {
+            title: 'Account video channels'
+          }
+        }
+      },
+      {
+        path: 'video-channels/create',
+        component: MyAccountVideoChannelCreateComponent,
+        data: {
+          meta: {
+            title: 'Create new video channel'
+          }
+        }
+      },
+      {
+        path: 'video-channels/update/:videoChannelId',
+        component: MyAccountVideoChannelUpdateComponent,
+        data: {
+          meta: {
+            title: 'Update video channel'
+          }
+        }
+      },
       {
         path: 'videos',
         component: MyAccountVideosComponent,
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/my-account/my-account-video-channels/my-account-video-channel-create.component.ts
new file mode 100644 (file)
index 0000000..3cfe747
--- /dev/null
@@ -0,0 +1,86 @@
+import { Component, OnInit } from '@angular/core'
+import { Router } from '@angular/router'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { VideoChannelCreate } from '../../../../../shared/models/videos'
+import {
+  VIDEO_CHANNEL_DESCRIPTION,
+  VIDEO_CHANNEL_DISPLAY_NAME,
+  VIDEO_CHANNEL_SUPPORT
+} from '@app/shared/forms/form-validators/video-channel'
+import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+
+@Component({
+  selector: 'my-account-video-channel-create',
+  templateUrl: './my-account-video-channel-edit.component.html',
+  styleUrls: [ './my-account-video-channel-edit.component.scss' ]
+})
+export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelEdit implements OnInit {
+  error: string
+
+  form: FormGroup
+  formErrors = {
+    'display-name': '',
+    'description': '',
+    'support': ''
+  }
+  validationMessages = {
+    'display-name': VIDEO_CHANNEL_DISPLAY_NAME.MESSAGES,
+    'description': VIDEO_CHANNEL_DESCRIPTION.MESSAGES,
+    'support': VIDEO_CHANNEL_SUPPORT.MESSAGES
+  }
+
+  constructor (
+    private notificationsService: NotificationsService,
+    private router: Router,
+    private formBuilder: FormBuilder,
+    private videoChannelService: VideoChannelService
+  ) {
+    super()
+  }
+
+  buildForm () {
+    this.form = this.formBuilder.group({
+      'display-name': [ '', VIDEO_CHANNEL_DISPLAY_NAME.VALIDATORS ],
+      description: [ '', VIDEO_CHANNEL_DESCRIPTION.VALIDATORS ],
+      support: [ '', VIDEO_CHANNEL_SUPPORT.VALIDATORS ]
+    })
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+  }
+
+  ngOnInit () {
+    this.buildForm()
+  }
+
+  formValidated () {
+    this.error = undefined
+
+    const body = this.form.value
+    const videoChannelCreate: VideoChannelCreate = {
+      displayName: body['display-name'],
+      description: body.description,
+      support: body.support
+    }
+
+    this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe(
+      () => {
+        this.notificationsService.success('Success', `Video channel ${videoChannelCreate.displayName} created.`)
+        this.router.navigate([ '/my-account', 'video-channels' ])
+      },
+
+      err => this.error = err.message
+    )
+  }
+
+  isCreation () {
+    return true
+  }
+
+  getFormButtonTitle () {
+    return 'Create this video channel'
+  }
+}
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.html b/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.html
new file mode 100644 (file)
index 0000000..adc969b
--- /dev/null
@@ -0,0 +1,42 @@
+<div class="form-sub-title" *ngIf="isCreation() === true">Create a video channel</div>
+<div class="form-sub-title" *ngIf="isCreation() === false">Update {{ videoChannel?.displayName }}</div>
+
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
+  <div class="form-group">
+    <label for="display-name">Display name</label>
+    <input
+      type="text" id="display-name"
+      formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
+    >
+    <div *ngIf="formErrors.display-name" class="form-error">
+      {{ formErrors.display-name }}
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="description">Description</label>
+    <textarea
+      id="description" formControlName="description"
+      [ngClass]="{ 'input-error': formErrors['description'] }"
+    ></textarea>
+    <div *ngIf="formErrors.description" class="form-error">
+      {{ formErrors.description }}
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="support">Support</label>
+    <my-help helpType="markdownEnhanced" preHtml="Short text to tell people how they can support your channel (membership platform...)."></my-help>
+    <my-markdown-textarea
+        id="support" formControlName="support" textareaWidth="500px" [previewColumn]="true" markdownType="enhanced"
+        [classes]="{ 'input-error': formErrors['support'] }"
+    ></my-markdown-textarea>
+    <div *ngIf="formErrors.support" class="form-error">
+      {{ formErrors.support }}
+    </div>
+  </div>
+
+  <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
+</form>
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
new file mode 100644 (file)
index 0000000..6fbb8ae
--- /dev/null
@@ -0,0 +1,27 @@
+@import '_variables';
+@import '_mixins';
+
+.form-sub-title {
+  margin-bottom: 20px;
+}
+
+input[type=text] {
+  @include peertube-input-text(340px);
+
+  display: block;
+}
+
+textarea {
+  @include peertube-textarea(500px, 150px);
+
+  display: block;
+}
+
+.peertube-select-container {
+  @include peertube-select-container(340px);
+}
+
+input[type=submit] {
+  @include peertube-button;
+  @include orange-button;
+}
\ No newline at end of file
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/my-account/my-account-video-channels/my-account-video-channel-edit.ts
new file mode 100644 (file)
index 0000000..e56f462
--- /dev/null
@@ -0,0 +1,6 @@
+import { FormReactive } from '@app/shared'
+
+export abstract class MyAccountVideoChannelEdit extends FormReactive {
+  abstract isCreation (): boolean
+  abstract getFormButtonTitle (): string
+}
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/my-account/my-account-video-channels/my-account-video-channel-update.component.ts
new file mode 100644 (file)
index 0000000..2b84159
--- /dev/null
@@ -0,0 +1,116 @@
+import { Component, OnInit, OnDestroy } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { VideoChannelUpdate } from '../../../../../shared/models/videos'
+import {
+  VIDEO_CHANNEL_DESCRIPTION,
+  VIDEO_CHANNEL_DISPLAY_NAME,
+  VIDEO_CHANNEL_SUPPORT
+} from '@app/shared/forms/form-validators/video-channel'
+import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+import { Subscription } from 'rxjs/Subscription'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+
+@Component({
+  selector: 'my-account-video-channel-update',
+  templateUrl: './my-account-video-channel-edit.component.html',
+  styleUrls: [ './my-account-video-channel-edit.component.scss' ]
+})
+export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
+  error: string
+
+  form: FormGroup
+  formErrors = {
+    'display-name': '',
+    'description': '',
+    'support': ''
+  }
+  validationMessages = {
+    'display-name': VIDEO_CHANNEL_DISPLAY_NAME.MESSAGES,
+    'description': VIDEO_CHANNEL_DESCRIPTION.MESSAGES,
+    'support': VIDEO_CHANNEL_SUPPORT.MESSAGES
+  }
+
+  private videoChannelToUpdate: VideoChannel
+  private paramsSub: Subscription
+
+  constructor (
+    private notificationsService: NotificationsService,
+    private router: Router,
+    private route: ActivatedRoute,
+    private formBuilder: FormBuilder,
+    private videoChannelService: VideoChannelService
+  ) {
+    super()
+  }
+
+  buildForm () {
+    this.form = this.formBuilder.group({
+      'display-name': [ '', VIDEO_CHANNEL_DISPLAY_NAME.VALIDATORS ],
+      description: [ '', VIDEO_CHANNEL_DESCRIPTION.VALIDATORS ],
+      support: [ '', VIDEO_CHANNEL_SUPPORT.VALIDATORS ]
+    })
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+  }
+
+  ngOnInit () {
+    this.buildForm()
+
+    this.paramsSub = this.route.params.subscribe(routeParams => {
+      const videoChannelId = routeParams['videoChannelId']
+
+      this.videoChannelService.getVideoChannel(videoChannelId).subscribe(
+        videoChannelToUpdate => {
+          this.videoChannelToUpdate = videoChannelToUpdate
+
+          this.form.patchValue({
+            'display-name': videoChannelToUpdate.displayName,
+            description: videoChannelToUpdate.description,
+            support: videoChannelToUpdate.support
+          })
+        },
+
+        err => this.error = err.message
+      )
+    })
+  }
+
+  ngOnDestroy () {
+    if (this.paramsSub) this.paramsSub.unsubscribe()
+  }
+
+  formValidated () {
+    this.error = undefined
+
+    const body = this.form.value
+    const videoChannelUpdate: VideoChannelUpdate = {
+      displayName: body['display-name'],
+      description: body.description,
+      support: body.support
+    }
+
+    this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.uuid, videoChannelUpdate).subscribe(
+      () => {
+        this.notificationsService.success('Success', `Video channel ${videoChannelUpdate.displayName} updated.`)
+        this.router.navigate([ '/my-account', 'video-channels' ])
+      },
+
+      err => this.error = err.message
+    )
+  }
+
+  isCreation () {
+    return false
+  }
+
+  getFormButtonTitle () {
+    return this.videoChannelToUpdate
+      ? 'Update ' + this.videoChannelToUpdate.displayName + ' video channel'
+      : 'Update'
+  }
+}
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.html
new file mode 100644 (file)
index 0000000..90c401b
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="video-channels-header">
+  <a class="create-button" routerLink="create">
+    <span class="icon icon-add"></span>
+    Create another video channel
+  </a>
+</div>
+
+<div class="video-channels">
+  <div *ngFor="let videoChannel of videoChannels" class="video-channel">
+    <a [routerLink]="[ '/video-channels', videoChannel.uuid ]">
+      <img [src]="videoChannel.avatarUrl" alt="Avatar" />
+    </a>
+
+    <div class="video-channel-info">
+      <a [routerLink]="[ '/video-channels', videoChannel.uuid ]" class="video-channel-names" title="Go to the channel">
+        <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
+        <!-- Hide the name for now, because it's an UUID not very friendly -->
+        <!--<div class="video-channel-name">{{ videoChannel.name }}</div>-->
+      </a>
+
+      <div class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
+    </div>
+
+    <div class="video-channel-buttons">
+      <my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
+
+      <my-edit-button [routerLink]="[ 'update', videoChannel.uuid ]"></my-edit-button>
+    </div>
+  </div>
+</div>
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.scss
new file mode 100644 (file)
index 0000000..bcb58ea
--- /dev/null
@@ -0,0 +1,72 @@
+@import '_variables';
+@import '_mixins';
+
+.create-button {
+  @include create-button;
+}
+
+/deep/ .action-button {
+  &.action-button-delete {
+    margin-right: 10px;
+  }
+}
+
+.video-channel {
+  display: flex;
+  min-height: 130px;
+  padding-bottom: 20px;
+  margin-bottom: 20px;
+  border-bottom: 1px solid #C6C6C6;
+
+  img {
+    @include avatar(80px);
+
+    margin-right: 10px;
+  }
+
+  .video-channel-info {
+    flex-grow: 1;
+
+    a.video-channel-names {
+      @include disable-default-a-behaviour;
+
+      display: flex;
+      color: #000;
+
+      .video-channel-display-name {
+        font-weight: $font-semibold;
+        font-size: 18px;
+      }
+
+      .video-channel-name {
+        font-size: 14px;
+        color: #777272;
+      }
+    }
+  }
+
+  .video-channel-buttons {
+    min-width: 190px;
+  }
+}
+
+.video-channels-header {
+  text-align: right;
+  margin: 20px 0 50px;
+}
+
+@media screen and (max-width: 800px) {
+  .video-channel {
+    flex-direction: column;
+    height: auto;
+    text-align: center;
+
+    img {
+      margin-right: 0;
+    }
+
+    .video-channel-buttons {
+      margin-top: 10px;
+    }
+  }
+}
diff --git a/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/my-account/my-account-video-channels/my-account-video-channels.component.ts
new file mode 100644 (file)
index 0000000..eb04094
--- /dev/null
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+import { User } from '@app/shared'
+
+@Component({
+  selector: 'my-account-video-channels',
+  templateUrl: './my-account-video-channels.component.html',
+  styleUrls: [ './my-account-video-channels.component.scss' ]
+})
+export class MyAccountVideoChannelsComponent implements OnInit{
+  videoChannels: VideoChannel[] = []
+
+  private user: User
+
+  constructor (
+    private authService: AuthService,
+    private notificationsService: NotificationsService,
+    private confirmService: ConfirmService,
+    private videoChannelService: VideoChannelService
+  ) {}
+
+  ngOnInit () {
+    this.user = this.authService.getUser()
+
+    this.loadVideoChannels()
+  }
+
+  async deleteVideoChannel (videoChannel: VideoChannel) {
+    const res = await this.confirmService.confirmWithInput(
+      `Do you really want to delete ${videoChannel.displayName}? It will delete all videos uploaded in this channel too.`,
+      'Please type the name of the video channel to confirm',
+      videoChannel.displayName,
+      'Delete'
+    )
+    if (res === false) return
+
+    this.videoChannelService.removeVideoChannel(videoChannel)
+      .subscribe(
+        status => {
+          this.loadVideoChannels()
+          this.notificationsService.success('Success', `Video channel ${videoChannel.name} deleted.`)
+        },
+
+        error => this.notificationsService.error('Error', error.message)
+      )
+  }
+
+  private loadVideoChannels () {
+    this.authService.userInformationLoaded
+        .flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account.id))
+        .subscribe(res => this.videoChannels = res.data)
+  }
+}
index a6cef361ee70b0d92106910090c8eb0c98dd8405..c1b53bcd5f8b999ed0d7c1f47e206745b125f8e5 100644 (file)
@@ -43,8 +43,6 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
 
   ngOnInit () {
     super.ngOnInit()
-
-    // this.generateSyndicationList()
   }
 
   ngOnDestroy () {
index 41afc1e5d2f3fc769da31f285bbe0a5e3d99f8f6..591d58cf92c13a91b48dce21bcfd36f504312686 100644 (file)
@@ -2,6 +2,8 @@
   <div class="sub-menu">
     <a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
 
+    <a routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a>
+
     <a routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
   </div>
 
index 981d3c6978a543362a080aeac9ea654137044b9c..ba9dea71ead37618823613d9844c0312ec2aa81a 100644 (file)
@@ -7,6 +7,9 @@ import { MyAccountSettingsComponent } from './my-account-settings/my-account-set
 import { MyAccountComponent } from './my-account.component'
 import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
 import { MyAccountProfileComponent } from '@app/my-account/my-account-settings/my-account-profile/my-account-profile.component'
+import { MyAccountVideoChannelsComponent } from '@app/my-account/my-account-video-channels/my-account-video-channels.component'
+import { MyAccountVideoChannelCreateComponent } from '@app/my-account/my-account-video-channels/my-account-video-channel-create.component'
+import { MyAccountVideoChannelUpdateComponent } from '@app/my-account/my-account-video-channels/my-account-video-channel-update.component'
 
 @NgModule({
   imports: [
@@ -20,7 +23,10 @@ import { MyAccountProfileComponent } from '@app/my-account/my-account-settings/m
     MyAccountChangePasswordComponent,
     MyAccountVideoSettingsComponent,
     MyAccountProfileComponent,
-    MyAccountVideosComponent
+    MyAccountVideosComponent,
+    MyAccountVideoChannelsComponent,
+    MyAccountVideoChannelCreateComponent,
+    MyAccountVideoChannelUpdateComponent
   ],
 
   exports: [
diff --git a/client/src/app/shared/forms/form-validators/video-channel.ts b/client/src/app/shared/forms/form-validators/video-channel.ts
new file mode 100644 (file)
index 0000000..6233d17
--- /dev/null
@@ -0,0 +1,34 @@
+import { Validators } from '@angular/forms'
+
+export const VIDEO_CHANNEL_DISPLAY_NAME = {
+  VALIDATORS: [
+    Validators.required,
+    Validators.minLength(3),
+    Validators.maxLength(120)
+  ],
+  MESSAGES: {
+    'required': 'Display name is required.',
+    'minlength': 'Display name must be at least 3 characters long.',
+    'maxlength': 'Display name cannot be more than 120 characters long.'
+  }
+}
+export const VIDEO_CHANNEL_DESCRIPTION = {
+  VALIDATORS: [
+    Validators.minLength(3),
+    Validators.maxLength(250)
+  ],
+  MESSAGES: {
+    'minlength': 'Description must be at least 3 characters long.',
+    'maxlength': 'Description cannot be more than 250 characters long.'
+  }
+}
+export const VIDEO_CHANNEL_SUPPORT = {
+  VALIDATORS: [
+    Validators.minLength(3),
+    Validators.maxLength(300)
+  ],
+  MESSAGES: {
+    'minlength': 'Support text must be at least 3 characters long.',
+    'maxlength': 'Support text cannot be more than 300 characters long.'
+  }
+}
index d8efcc1717271b01963d39e338dd4bc00ad343f1..3533a0e9cb28660cbea5e2286a316d4eb2c3e86e 100644 (file)
@@ -5,12 +5,14 @@ import { Observable } from 'rxjs/Observable'
 import { RestExtractor } from '../rest/rest-extractor.service'
 import { RestService } from '../rest/rest.service'
 import { HttpClient } from '@angular/common/http'
-import { VideoChannel as VideoChannelServer } from '../../../../../shared/models/videos'
+import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos'
 import { AccountService } from '../account/account.service'
 import { ResultList } from '../../../../../shared'
 import { VideoChannel } from './video-channel.model'
 import { ReplaySubject } from 'rxjs/ReplaySubject'
 import { environment } from '../../../environments/environment'
+import { UserService } from '@app/+admin/users/shared/user.service'
+import { User } from '@app/shared'
 
 @Injectable()
 export class VideoChannelService {
@@ -37,6 +39,24 @@ export class VideoChannelService {
                .catch((res) => this.restExtractor.handleError(res))
   }
 
+  createVideoChannel (videoChannel: VideoChannelCreate) {
+    return this.authHttp.post(VideoChannelService.BASE_VIDEO_CHANNEL_URL, videoChannel)
+               .map(this.restExtractor.extractDataBool)
+               .catch(err => this.restExtractor.handleError(err))
+  }
+
+  updateVideoChannel (videoChannelUUID: string, videoChannel: VideoChannelUpdate) {
+    return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID, videoChannel)
+               .map(this.restExtractor.extractDataBool)
+               .catch(err => this.restExtractor.handleError(err))
+  }
+
+  removeVideoChannel (videoChannel: VideoChannel) {
+    return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.uuid)
+               .map(this.restExtractor.extractDataBool)
+               .catch(err => this.restExtractor.handleError(err))
+  }
+
   private extractVideoChannels (result: ResultList<VideoChannelServer>) {
     const videoChannels: VideoChannel[] = []
 
index f2d9f720126537e8466171cafea95cc20759a44b..9aef0c56de686af40e6d015633b3aced79b5327d 100644 (file)
@@ -118,12 +118,12 @@ label {
   align-items: center;
   margin-bottom: 30px;
 
-  .admin-sub-title {
+  .form-sub-title {
     flex-grow: 1;
   }
 }
 
-.admin-sub-title {
+.form-sub-title {
   font-size: 20px;
   font-weight: bold;
 }
index 675cccfebea5960d802f1b6a8033e82ee13d70d5..ffbedd3f58afb9b1d71b761f2ccc251badff6856 100644 (file)
       margin-bottom: 0;
     }
   }
+}
+
+@mixin create-button {
+  @include peertube-button-link;
+  @include orange-button;
+
+  .icon.icon-add {
+    @include icon(22px);
+
+    margin-right: 3px;
+    background-image: url('/assets/images/admin/add.svg');
+  }
 }
\ No newline at end of file
index 6241aaa5cbac730e98b3cb34839f615e89947e5d..263eb2a8a1c66454961e6971914f3391d664c008 100644 (file)
@@ -138,7 +138,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
         transaction: t
       }
 
-      if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
+      if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.displayName)
       if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
       if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support)
 
index d019c4e719b44f7c51fc27f91711fe8e284d60b6..51050de9b2a927a7fcb3f238d7f5f3f95680b71a 100644 (file)
@@ -17,9 +17,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
     const userCreated = await userToCreate.save(userOptions)
     const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t)
 
-    const videoChannelName = `Default ${userCreated.username} channel`
+    const videoChannelDisplayName = `Default ${userCreated.username} channel`
     const videoChannelInfo = {
-      name: videoChannelName
+      displayName: videoChannelDisplayName
     }
     const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
 
index 9f7ed929738c56d7f915cb59d70279b5afd6debd..600316cda66019008e211cf4e8e41551901ae37c 100644 (file)
@@ -14,7 +14,7 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
   const actorInstanceCreated = await actorInstance.save({ transaction: t })
 
   const videoChannelData = {
-    name: videoChannelInfo.name,
+    name: videoChannelInfo.displayName,
     description: videoChannelInfo.description,
     support: videoChannelInfo.support,
     accountId: account.id,
index a70f196df45dc165db08e49aabf6dddd5eb8cbae..3af20a1b4d7571ee2af0e2fc91e2d65985dd2edc 100644 (file)
@@ -27,7 +27,7 @@ const listVideoAccountChannelsValidator = [
 ]
 
 const videoChannelsAddValidator = [
-  body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
+  body('displayName').custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
   body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
   body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'),
 
@@ -42,7 +42,7 @@ const videoChannelsAddValidator = [
 
 const videoChannelsUpdateValidator = [
   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
-  body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'),
+  body('displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
   body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
   body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'),
 
index 7cda879ed9cb10d2fa0b737609619595c722dc92..40a8e97cc981d9be17eb7fbd0d94e2bd34f04c1a 100644 (file)
@@ -25,7 +25,7 @@ import { User } from '../../../../shared/models/users'
 
 const expect = chai.expect
 
-describe('Test videos API validator', function () {
+describe('Test video channels API validator', function () {
   const videoChannelPath = '/api/v1/video-channels'
   let server: ServerInfo
   let accessTokenUser: string
@@ -85,7 +85,7 @@ describe('Test videos API validator', function () {
 
   describe('When adding a video channel', function () {
     const baseCorrectParams = {
-      name: 'hello',
+      displayName: 'hello',
       description: 'super description',
       support: 'super support text'
     }
@@ -105,13 +105,13 @@ describe('Test videos API validator', function () {
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
-    it('Should fail without name', async function () {
-      const fields = omit(baseCorrectParams, 'name')
+    it('Should fail without name', async function () {
+      const fields = omit(baseCorrectParams, 'displayName')
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(25) })
+      const fields = immutableAssign(baseCorrectParams, { displayName: 'super'.repeat(25) })
       await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
     })
 
@@ -138,7 +138,7 @@ describe('Test videos API validator', function () {
 
   describe('When updating a video channel', function () {
     const baseCorrectParams = {
-      name: 'hello',
+      displayName: 'hello',
       description: 'super description'
     }
     let path: string
@@ -168,7 +168,7 @@ describe('Test videos API validator', function () {
     })
 
     it('Should fail with a long name', async function () {
-      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(25) })
+      const fields = immutableAssign(baseCorrectParams, { displayName: 'super'.repeat(25) })
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
index e462a2d47485b77593d55604510f798fb8f603a2..94d627e263d53b92ab3eb76adfcbc8d0fe22db61 100644 (file)
@@ -59,7 +59,7 @@ describe('Test multiple servers', function () {
 
     {
       const videoChannel = {
-        name: 'my channel',
+        displayName: 'my channel',
         description: 'super channel'
       }
       await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel)
index d24b8ab0bdef15171007b652e5caae030b668807..585b6a2b59a37b9c40307b0a14e01015c088d000 100644 (file)
@@ -59,7 +59,7 @@ describe('Test video channels', function () {
     this.timeout(10000)
 
     const videoChannel = {
-      name: 'second video channel',
+      displayName: 'second video channel',
       description: 'super video channel description',
       support: 'super video channel support text'
     }
@@ -125,7 +125,7 @@ describe('Test video channels', function () {
     this.timeout(5000)
 
     const videoChannelAttributes = {
-      name: 'video channel updated',
+      displayName: 'video channel updated',
       description: 'video channel description updated',
       support: 'video channel support text updated'
     }
index 978e21b199186fe04d8354d9ecf7afe9a447781c..021c4c420c62ab92d1a8b996a349c61badda1b73 100644 (file)
@@ -1,10 +1,5 @@
 import * as request from 'supertest'
-
-type VideoChannelAttributes = {
-  name?: string
-  description?: string
-  support?: string
-}
+import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared/models/videos'
 
 function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
   const path = '/api/v1/video-channels'
@@ -34,14 +29,14 @@ function getAccountVideoChannelsList (url: string, accountId: number | string, s
 function addVideoChannel (
   url: string,
   token: string,
-  videoChannelAttributesArg: VideoChannelAttributes,
+  videoChannelAttributesArg: VideoChannelCreate,
   expectedStatus = 200
 ) {
   const path = '/api/v1/video-channels/'
 
   // Default attributes
   let attributes = {
-    name: 'my super video channel',
+    displayName: 'my super video channel',
     description: 'my super channel description',
     support: 'my super channel support'
   }
@@ -59,13 +54,13 @@ function updateVideoChannel (
   url: string,
   token: string,
   channelId: number | string,
-  attributes: VideoChannelAttributes,
+  attributes: VideoChannelUpdate,
   expectedStatus = 204
 ) {
   const body = {}
   const path = '/api/v1/video-channels/' + channelId
 
-  if (attributes.name) body['name'] = attributes.name
+  if (attributes.displayName) body['displayName'] = attributes.displayName
   if (attributes.description) body['description'] = attributes.description
   if (attributes.support) body['support'] = attributes.support
 
index cd6bae96573a98e91d7ae6d7b63caba5454ccc77..08cd5fb84739546540dd5df9b21a3f0680cf2e3b 100644 (file)
@@ -1,5 +1,5 @@
 export interface VideoChannelCreate {
-  name: string
+  displayName: string
   description?: string
   support?: string
 }
index 73a0a670941faf23b49d5096eb499107a0ab79d9..3626ce8a900991a2183036e98237df8ad734a9ff 100644 (file)
@@ -1,5 +1,5 @@
 export interface VideoChannelUpdate {
-  name: string
+  displayName: string
   description?: string
   support?: string
 }