Finish admin design
authorChocobozzz <florian.bigard@gmail.com>
Fri, 8 Dec 2017 16:31:21 +0000 (17:31 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 8 Dec 2017 16:31:21 +0000 (17:31 +0100)
19 files changed:
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss [new file with mode: 0644]
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
client/src/app/core/auth/auth.service.ts
client/src/app/shared/video/abstract-video-list.ts
client/src/app/videos/+video-edit/shared/video-description.component.html
client/src/app/videos/+video-edit/shared/video-description.component.scss
client/src/app/videos/+video-edit/shared/video-description.component.ts
client/src/sass/application.scss
server/controllers/api/videos/index.ts
server/helpers/custom-validators/activitypub/videos.ts
server/helpers/custom-validators/videos.ts
server/lib/activitypub/process/misc.ts
server/models/video/video.ts
server/tests/api/multiple-servers.ts
server/tests/api/single-server.ts

index 3e70b418cb373165c28c95fb254bf29c14066ffd..2b6cc91130e77da4d9ad08d0c063d581bd3c9f61 100644 (file)
@@ -6,7 +6,7 @@
   <p-column field="following.host" header="Host"></p-column>
   <p-column field="state" header="State"></p-column>
   <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
-  <p-column header="Unfollow" styleClass="action-cell">
+  <p-column styleClass="action-cell">
     <ng-template pTemplate="body" let-following="rowData">
       <my-delete-button (click)="removeFollowing(following)"></my-delete-button>
     </ng-template>
index 29103c06b77d2318c9e16c1c1ce6f50154368336..7aa5f425499af43f2bcaefa4f5d7cc65a1be0e5b 100644 (file)
@@ -8,7 +8,7 @@
 >
   <p-column field="id" header="ID" [style]="{ width: '40px' }"></p-column>
   <p-column field="category" header="Category" [style]="{ width: '100px' }"></p-column>
-  <p-column field="handlerName" header="Handler name" [style]="{ width: '150px' }"></p-column>
+  <p-column field="handlerName" header="Handler name" [style]="{ width: '200px' }"></p-column>
   <p-column header="Input data">
     <ng-template pTemplate="body" let-job="rowData">
       <pre>{{ job.handlerInputData }}</pre>
index ab0a9d99f23412e6400d1ad959ac49e6aabd8ba3..d655a5e9b2da2ac6c7368815ba05fdb36661ce99 100644 (file)
@@ -1,24 +1,19 @@
-<div class="row">
-  <div class="content-padding">
-
-  <h3>Video abuses list</h3>
-
-  <p-dataTable
-      [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
-      sortField="id" (onLazyLoad)="loadLazy($event)"
-  >
-    <p-column field="id" header="ID" [sortable]="true"></p-column>
-    <p-column field="reason" header="Reason"></p-column>
-    <p-column field="reporterServerHost" header="Reporter server host"></p-column>
-    <p-column field="reporterUsername" header="Reporter username"></p-column>
-    <p-column field="videoName" header="Video name"></p-column>
-    <p-column header="Video" styleClass="action-cell">
-      <ng-template pTemplate="body" let-videoAbuse="rowData">
-        <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a>
-      </ng-template>
-    </p-column>
-    <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
-  </p-dataTable>
-
-  </div>
+<div class="admin-sub-header">
+  <div class="admin-sub-title">Video abuses list</div>
 </div>
+
+<p-dataTable
+    [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+    sortField="id" (onLazyLoad)="loadLazy($event)"
+>
+  <p-column field="id" header="ID" [sortable]="true"></p-column>
+  <p-column field="reason" header="Reason"></p-column>
+  <p-column field="reporterServerHost" header="Reporter server host"></p-column>
+  <p-column field="reporterUsername" header="Reporter username"></p-column>
+  <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
+  <p-column header="Video">
+    <ng-template pTemplate="body" let-videoAbuse="rowData">
+      <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoName }}</a>
+    </ng-template>
+  </p-column>
+</p-dataTable>
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss
new file mode 100644 (file)
index 0000000..6a47626
--- /dev/null
@@ -0,0 +1,6 @@
+/deep/ a {
+
+  &, &:hover, &:active, &:focus {
+    color: #000;
+  }
+}
index 654603d016580000c4417c11d0c7ed378cce7870..b4d3bbd24f28f1ad230c777d1feccba0d8678f3b 100644 (file)
@@ -8,7 +8,8 @@ import { VideoAbuse } from '../../../../../../shared'
 
 @Component({
   selector: 'my-video-abuse-list',
-  templateUrl: './video-abuse-list.component.html'
+  templateUrl: './video-abuse-list.component.html',
+  styleUrls: [ './video-abuse-list.component.scss']
 })
 export class VideoAbuseListComponent extends RestTable implements OnInit {
   videoAbuses: VideoAbuse[] = []
index 05d1167984a137db9763f3d8d6bc76fdde4678d5..1d813fa07bac4fbb29af5a47732bcd4e99469f53 100644 (file)
@@ -18,7 +18,7 @@
       <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
       <p-column header="Delete" styleClass="action-cell">
         <ng-template pTemplate="body" let-entry="rowData">
-          <span (click)="removeVideoFromBlacklist(entry)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this video from blacklist"></span>
+          <my-delete-button (click)="removeVideoFromBlacklist(entry)"></my-delete-button>
         </ng-template>
       </p-column>
     </p-dataTable>
index 0db197f0201d8e8ec665523a4dfaeba8dc13cbb7..e887dde1ff5be7c023144ff277140e1c88d2b72c 100644 (file)
@@ -169,19 +169,15 @@ export class AuthService {
 
     return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
                     .map(res => this.handleRefreshToken(res))
-                    .catch(res => {
-                      // The refresh token is invalid?
-                      if (res.status === 400 && res.error.error === 'invalid_grant') {
-                        console.error('Cannot refresh token -> logout...')
-                        this.logout()
-                        this.router.navigate(['/login'])
-
-                        return Observable.throw({
-                          error: 'You need to reconnect.'
-                        })
-                      }
-
-                      return this.restExtractor.handleError(res)
+                    .catch(err => {
+                      console.error(err)
+                      console.log('Cannot refresh token -> logout...')
+                      this.logout()
+                      this.router.navigate(['/login'])
+
+                      return Observable.throw({
+                        error: 'You need to reconnect.'
+                      })
                     })
   }
 
index ee1ed2cb27fd28d425a8ce79ea7b897a70d63866..ba1635a180966c2c5a9150b47fbfad705ab8c57d 100644 (file)
@@ -62,7 +62,7 @@ export abstract class AbstractVideoList implements OnInit {
     observable.subscribe(
       ({ videos, totalVideos }) => {
         // Paging is too high, return to the first one
-        if (totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
+        if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
           this.pagination.currentPage = 1
           this.setNewRouteParams()
           return this.reloadVideos()
@@ -82,6 +82,10 @@ export abstract class AbstractVideoList implements OnInit {
   }
 
   protected hasMoreVideos () {
+    // No results
+    if (this.pagination.totalItems === 0) return false
+
+    // Not loaded yet
     if (!this.pagination.totalItems) return true
 
     const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
index da66a975332f424d01ea3c2a18b20a2edf7505ea..5d05467be29ae37d17ae5c51a96ad7453cf3fc87 100644 (file)
@@ -1,6 +1,6 @@
 <textarea
   [(ngModel)]="description" (ngModelChange)="onModelChange()"
-  id="description" placeholder="My super video">
+  id="description" name="description">
 </textarea>
 
 <tabset #staticTabs class="previews">
index 8155cbca71859915a1bdc2ed3b3f8d71e55932cb..2a4c8d189318781c671f8919920a352fdc87aa26 100644 (file)
@@ -4,4 +4,21 @@ textarea {
   padding: 5px 15px;
   font-size: 15px;
   height: 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;
+  }
+}
+
index 8dfb74b2a12299eb9465911b287f950b3e5ea7a8..9b77a27e6056838f0379feaf8474ef4f1d625dcc 100644 (file)
@@ -60,6 +60,8 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
   }
 
   private updateDescriptionPreviews () {
+    if (!this.description) return
+
     this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
     this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
   }
index ecbb8dac5e1bf840190bc47992475d14215bb30e..5a4aa4cd97e2fb8d7d07ef03f5a660163bee49de 100644 (file)
@@ -165,7 +165,7 @@ p-datatable {
 
   td {
     border: 1px solid #E5E5E5 !important;
-    padding: 15px;
+    padding-left: 15px !important;
   }
 
   tr {
@@ -185,6 +185,10 @@ p-datatable {
     &:first-child td {
       border-top: none !important;
     }
+
+    &:last-child td {
+      border-bottom: none !important;
+    }
   }
 
   th {
@@ -198,6 +202,7 @@ p-datatable {
     &.ui-state-active, &.ui-sortable-column:hover {
       background-color: #f0f0f0 !important;
       border: 1px solid #f0f0f0 !important;
+      border-width: 0 1px !important;
     }
   }
 
@@ -208,17 +213,10 @@ p-datatable {
   }
 
   p-paginator {
-    overflow: hidden;
-    display: block;
-    padding-top: 2px;
-    border: 1px solid #f0f0f0 !important;
-    border-top: none !important;
-
     .ui-paginator-bottom {
       position: relative;
       border: none !important;
-      border-top: 1px solid #f0f0f0 !important;
-      box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.16);
+      border: 1px solid #f0f0f0 !important;
       height: 40px;
       display: flex;
       justify-content: center;
@@ -298,11 +296,6 @@ p-datatable {
       font-weight: $font-semibold !important;
     }
   }
-
-  .tab-content {
-    min-height: 75px;
-    padding: 15px;
-  }
 }
 
 
index 0f71a7f7fc7dba942d409f480b20b4a5b4515821..63de662a7a528a4de2f74ccf4a474b3c00a132b2 100644 (file)
@@ -280,7 +280,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
       if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
       if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
       if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
-      if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy)
+      if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', parseInt(videoInfoToUpdate.privacy.toString(), 10))
       if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
 
       const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
@@ -298,9 +298,9 @@ async function updateVideo (req: express.Request, res: express.Response) {
       }
 
       // Video is not private anymore, send a create action to remote servers
-      if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) {
-        await sendAddVideo(videoInstance, t)
-        await shareVideoByServer(videoInstance, t)
+      if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
+        await sendAddVideo(videoInstanceUpdated, t)
+        await shareVideoByServer(videoInstanceUpdated, t)
       }
     })
 
index 12c672fd2ea214fd3b3398da7ec488645d820f0e..2ed2988f55e04fd393a0b123f4a34ed2e604c222 100644 (file)
@@ -49,14 +49,14 @@ function isVideoTorrentObjectValid (video: any) {
     isActivityPubVideoDurationValid(video.duration) &&
     isUUIDValid(video.uuid) &&
     setValidRemoteTags(video) &&
-    isRemoteIdentifierValid(video.category) &&
-    isRemoteIdentifierValid(video.licence) &&
+    (!video.category || isRemoteIdentifierValid(video.category)) &&
+    (!video.licence || isRemoteIdentifierValid(video.licence)) &&
     (!video.language || isRemoteIdentifierValid(video.language)) &&
     isVideoViewsValid(video.views) &&
     isVideoNSFWValid(video.nsfw) &&
     isDateValid(video.published) &&
     isDateValid(video.updated) &&
-    isRemoteVideoContentValid(video.mediaType, video.content) &&
+    (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
     isRemoteVideoIconValid(video.icon) &&
     setValidRemoteVideoUrls(video) &&
     video.url.length !== 0
index f13178c54d62fc195a176a985b9aea0184fa5eb4..4fc460699e3d921e5cbb20883e4bdd94f01d7841 100644 (file)
@@ -9,16 +9,17 @@ import { VIDEO_PRIVACIES } from '../../initializers/constants'
 import { database as db } from '../../initializers/database'
 import { VideoInstance } from '../../models/video/video-interface'
 import { exists, isArray } from './misc'
+import isInt = require('validator/lib/isInt')
 
 const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
 const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
 
 function isVideoCategoryValid (value: number) {
-  return VIDEO_CATEGORIES[value] !== undefined
+  return value === null || VIDEO_CATEGORIES[value] !== undefined
 }
 
 function isVideoLicenceValid (value: number) {
-  return VIDEO_LICENCES[value] !== undefined
+  return value === null || VIDEO_LICENCES[value] !== undefined
 }
 
 function isVideoLanguageValid (value: number) {
@@ -38,7 +39,7 @@ function isVideoTruncatedDescriptionValid (value: string) {
 }
 
 function isVideoDescriptionValid (value: string) {
-  return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION)
+  return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
 }
 
 function isVideoNameValid (value: string) {
@@ -84,7 +85,7 @@ function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
 }
 
 function isVideoPrivacyValid (value: string) {
-  return VIDEO_PRIVACIES[value] !== undefined
+  return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined
 }
 
 function isVideoFileInfoHashValid (value: string) {
index f20e588ab6460f147984a717c326e0de119bcdd7..0baa22c2617d5dc0c3de9fb13eafeac43a16b870 100644 (file)
@@ -41,15 +41,30 @@ async function videoActivityObjectToDBAttributes (
     language = parseInt(videoObject.language.identifier, 10)
   }
 
+  let category = null
+  if (videoObject.category) {
+    category = parseInt(videoObject.category.identifier, 10)
+  }
+
+  let licence = null
+  if (videoObject.licence) {
+    licence = parseInt(videoObject.licence.identifier, 10)
+  }
+
+  let description = null
+  if (videoObject.content) {
+    description = videoObject.content
+  }
+
   const videoData: VideoAttributes = {
     name: videoObject.name,
     uuid: videoObject.uuid,
     url: videoObject.id,
-    category: parseInt(videoObject.category.identifier, 10),
-    licence: parseInt(videoObject.licence.identifier, 10),
+    category,
+    licence,
     language,
+    description,
     nsfw: videoObject.nsfw,
-    description: videoObject.content,
     channelId: videoChannel.id,
     duration: parseInt(duration, 10),
     createdAt: new Date(videoObject.published),
index 8b1eb1f96cb554a66dc616ce2b04abb4c2bdffdf..d46fdeebe162aa852a07e38ecdee984a85da534c 100644 (file)
@@ -564,6 +564,22 @@ toActivityPubObject = function (this: VideoInstance) {
     }
   }
 
+  let category
+  if (this.category) {
+    category = {
+      identifier: this.category + '',
+      name: this.getCategoryLabel()
+    }
+  }
+
+  let licence
+  if (this.licence) {
+    licence = {
+      identifier: this.licence + '',
+      name: this.getLicenceLabel()
+    }
+  }
+
   let likesObject
   let dislikesObject
 
@@ -635,14 +651,8 @@ toActivityPubObject = function (this: VideoInstance) {
     duration: 'PT' + this.duration + 'S',
     uuid: this.uuid,
     tag,
-    category: {
-      identifier: this.category + '',
-      name: this.getCategoryLabel()
-    },
-    licence: {
-      identifier: this.licence + '',
-      name: this.getLicenceLabel()
-    },
+    category,
+    licence,
     language,
     views: this.views,
     nsfw: this.nsfw,
index c7f19f26160199edae8618b68db607e8bea21715..c9f74cc8cbf8142be37e1c09f7882a00d3efd96f 100644 (file)
@@ -2,6 +2,8 @@
 
 import 'mocha'
 import * as chai from 'chai'
+import { join } from "path"
+import * as request from 'supertest'
 
 import {
   dateIsValid,
@@ -707,6 +709,50 @@ describe('Test multiple servers', function () {
     })
   })
 
+  describe('With minimum parameters', function () {
+    it('Should upload and propagate the video', async function () {
+      this.timeout(50000)
+
+      const path = '/api/v1/videos/upload'
+
+      const req = request(servers[1].url)
+        .post(path)
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + servers[1].accessToken)
+        .field('name', 'minimum parameters')
+        .field('privacy', '1')
+        .field('nsfw', 'false')
+        .field('channelId', '1')
+
+      const filePath = join(__dirname, '..', 'api', 'fixtures', 'video_short.webm')
+
+      await req.attach('videofile', filePath)
+        .expect(200)
+
+      await wait(25000)
+
+      for (const server of servers) {
+        const res = await getVideosList(server.url)
+        const video = res.body.data.find(v => v.name === 'minimum parameters')
+
+        expect(video.name).to.equal('minimum parameters')
+        expect(video.category).to.equal(null)
+        expect(video.categoryLabel).to.equal('Misc')
+        expect(video.licence).to.equal(null)
+        expect(video.licenceLabel).to.equal('Unknown')
+        expect(video.language).to.equal(null)
+        expect(video.languageLabel).to.equal('Unknown')
+        expect(video.nsfw).to.not.be.ok
+        expect(video.description).to.equal(null)
+        expect(video.serverHost).to.equal('localhost:9002')
+        expect(video.accountName).to.equal('root')
+        expect(video.tags).to.deep.equal([ ])
+        expect(dateIsValid(video.createdAt)).to.be.true
+        expect(dateIsValid(video.updatedAt)).to.be.true
+      }
+    })
+  })
+
   after(async function () {
     killallServers(servers)
 
index e99955ef4356551c782bedd3ab66b35e5fad0769..91460c7ae97c907caa9b4fca9c840d184e48dc28 100644 (file)
@@ -694,43 +694,6 @@ describe('Test a single server', function () {
     expect(video.dislikes).to.equal(1)
   })
 
-  it('Should upload a video with minimum parameters', async function () {
-    const path = '/api/v1/videos/upload'
-
-    const req = request(server.url)
-      .post(path)
-      .set('Accept', 'application/json')
-      .set('Authorization', 'Bearer ' + server.accessToken)
-      .field('name', 'minimum parameters')
-      .field('privacy', '1')
-      .field('nsfw', 'false')
-      .field('channelId', '1')
-
-    const filePath = join(__dirname, '..', 'api', 'fixtures', 'video_short.webm')
-
-    await req.attach('videofile', filePath)
-      .expect(200)
-
-    const res = await getVideosList(server.url)
-    const video = res.body.data.find(v => v.name === 'minimum parameters')
-
-    expect(video.name).to.equal('minimum parameters')
-    expect(video.category).to.equal(null)
-    expect(video.categoryLabel).to.equal('Misc')
-    expect(video.licence).to.equal(null)
-    expect(video.licenceLabel).to.equal('Unknown')
-    expect(video.language).to.equal(null)
-    expect(video.languageLabel).to.equal('Unknown')
-    expect(video.nsfw).to.not.be.ok
-    expect(video.description).to.equal(null)
-    expect(video.serverHost).to.equal('localhost:9001')
-    expect(video.accountName).to.equal('root')
-    expect(video.isLocal).to.be.true
-    expect(video.tags).to.deep.equal([ ])
-    expect(dateIsValid(video.createdAt)).to.be.true
-    expect(dateIsValid(video.updatedAt)).to.be.true
-  })
-
   after(async function () {
     killallServers([ server ])