aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2019-12-26 11:52:46 +0100
committerRigel Kent <sendmemail@rigelk.eu>2019-12-26 11:52:52 +0100
commitc06af5012ecc925ca924e6e20db3a1d909b1148e (patch)
tree88a3595bfd0a68e8f5314737a22f9516d9790c9b
parentdef2a70b7e5ee807d7b532df8c9d33d17d24ccbb (diff)
downloadPeerTube-c06af5012ecc925ca924e6e20db3a1d909b1148e.tar.gz
PeerTube-c06af5012ecc925ca924e6e20db3a1d909b1148e.tar.zst
PeerTube-c06af5012ecc925ca924e6e20db3a1d909b1148e.zip
Add playlist search option and search input for add-to-video-playlist dropdown
fixes #2138
-rw-r--r--client/src/app/menu/avatar-notification.component.scss1
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.html4
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.scss10
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.ts20
-rw-r--r--client/src/app/shared/video-playlist/video-playlist.service.ts10
-rw-r--r--server/controllers/api/accounts.ts7
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts13
-rw-r--r--server/models/video/video-playlist.ts31
8 files changed, 85 insertions, 11 deletions
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss
index 2ca7f24dc..ebb2a5424 100644
--- a/client/src/app/menu/avatar-notification.component.scss
+++ b/client/src/app/menu/avatar-notification.component.scss
@@ -34,6 +34,7 @@
34 34
35 & > my-user-notifications:nth-child(2) { 35 & > my-user-notifications:nth-child(2) {
36 overflow-y: auto; 36 overflow-y: auto;
37 flex-grow: 1;
37 } 38 }
38 } 39 }
39 40
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
index 0cc8af345..ba30953b9 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
@@ -41,6 +41,10 @@
41 </div> 41 </div>
42 </div> 42 </div>
43 43
44 <div class="input-container">
45 <input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
46 </div>
47
44 <div class="playlists"> 48 <div class="playlists">
45 <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> 49 <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
46 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> 50 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox>
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
index 090b530cf..5f9bb51a7 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
@@ -53,6 +53,15 @@
53 overflow-y: auto; 53 overflow-y: auto;
54} 54}
55 55
56.input-container {
57 display: flex;
58
59 input {
60 flex-grow: 1;
61 margin: 0 15px 10px 15px;
62 }
63}
64
56.playlist { 65.playlist {
57 display: flex; 66 display: flex;
58 cursor: pointer; 67 cursor: pointer;
@@ -76,7 +85,6 @@
76.new-playlist-button, 85.new-playlist-button,
77.new-playlist-block { 86.new-playlist-block {
78 padding-top: 10px; 87 padding-top: 10px;
79 margin-top: 10px;
80 border-top: 1px solid $separator-border-color; 88 border-top: 1px solid $separator-border-color;
81} 89}
82 90
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
index 6380c2e51..25ba8cbca 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
@@ -1,7 +1,8 @@
1import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' 1import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
2import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 2import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { forkJoin } from 'rxjs' 4import { forkJoin, Subject } from 'rxjs'
5import { debounceTime } from 'rxjs/operators'
5import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' 6import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models'
6import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms' 7import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
7import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -29,6 +30,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
29 @Input() lazyLoad = false 30 @Input() lazyLoad = false
30 31
31 isNewPlaylistBlockOpened = false 32 isNewPlaylistBlockOpened = false
33 videoPlaylistSearch: string
34 videoPlaylistSearchChanged = new Subject<string>()
32 videoPlaylists: PlaylistSummary[] = [] 35 videoPlaylists: PlaylistSummary[] = []
33 timestampOptions: { 36 timestampOptions: {
34 startTimestampEnabled: boolean 37 startTimestampEnabled: boolean
@@ -58,6 +61,13 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
58 this.buildForm({ 61 this.buildForm({
59 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME 62 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
60 }) 63 })
64
65 this.videoPlaylistSearchChanged
66 .pipe(
67 debounceTime(500))
68 .subscribe(() => {
69 this.load()
70 })
61 } 71 }
62 72
63 ngOnChanges (simpleChanges: SimpleChanges) { 73 ngOnChanges (simpleChanges: SimpleChanges) {
@@ -74,6 +84,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
74 84
75 reload () { 85 reload () {
76 this.videoPlaylists = [] 86 this.videoPlaylists = []
87 this.videoPlaylistSearch = undefined
77 88
78 this.init() 89 this.init()
79 90
@@ -82,11 +93,12 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
82 93
83 load () { 94 load () {
84 forkJoin([ 95 forkJoin([
85 this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined,'-updatedAt'), 96 this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined, '-updatedAt', this.videoPlaylistSearch),
86 this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) 97 this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
87 ]) 98 ])
88 .subscribe( 99 .subscribe(
89 ([ playlistsResult, existResult ]) => { 100 ([ playlistsResult, existResult ]) => {
101 this.videoPlaylists = []
90 for (const playlist of playlistsResult.data) { 102 for (const playlist of playlistsResult.data) {
91 const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id) 103 const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id)
92 104
@@ -178,6 +190,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
178 return `(${start}-${stop})` 190 return `(${start}-${stop})`
179 } 191 }
180 192
193 onVideoPlaylistSearchChanged () {
194 this.videoPlaylistSearchChanged.next()
195 }
196
181 private removeVideoFromPlaylist (playlist: PlaylistSummary) { 197 private removeVideoFromPlaylist (playlist: PlaylistSummary) {
182 if (!playlist.playlistElementId) return 198 if (!playlist.playlistElementId) return
183 199
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts
index 2945b4959..5f74dcd4c 100644
--- a/client/src/app/shared/video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/video-playlist/video-playlist.service.ts
@@ -59,7 +59,12 @@ export class VideoPlaylistService {
59 ) 59 )
60 } 60 }
61 61
62 listAccountPlaylists (account: Account, componentPagination: ComponentPagination, sort: string): Observable<ResultList<VideoPlaylist>> { 62 listAccountPlaylists (
63 account: Account,
64 componentPagination: ComponentPagination,
65 sort: string,
66 search?: string
67 ): Observable<ResultList<VideoPlaylist>> {
63 const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' 68 const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
64 const pagination = componentPagination 69 const pagination = componentPagination
65 ? this.restService.componentPaginationToRestPagination(componentPagination) 70 ? this.restService.componentPaginationToRestPagination(componentPagination)
@@ -67,6 +72,7 @@ export class VideoPlaylistService {
67 72
68 let params = new HttpParams() 73 let params = new HttpParams()
69 params = this.restService.addRestGetParams(params, pagination, sort) 74 params = this.restService.addRestGetParams(params, pagination, sort)
75 if (search) params = this.restService.addObjectParams(params, { search })
70 76
71 return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) 77 return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
72 .pipe( 78 .pipe(
@@ -213,8 +219,8 @@ export class VideoPlaylistService {
213 219
214 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { 220 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
215 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' 221 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
216 let params = new HttpParams()
217 222
223 let params = new HttpParams()
218 params = this.restService.addObjectParams(params, { videoIds }) 224 params = this.restService.addObjectParams(params, { videoIds })
219 225
220 return this.authHttp.get<VideoExistInPlaylist>(url, { params }) 226 return this.authHttp.get<VideoExistInPlaylist>(url, { params })
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 5a1d652f2..c49da3c0a 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -27,7 +27,10 @@ import { VideoChannelModel } from '../../models/video/video-channel'
27import { JobQueue } from '../../lib/job-queue' 27import { JobQueue } from '../../lib/job-queue'
28import { logger } from '../../helpers/logger' 28import { logger } from '../../helpers/logger'
29import { VideoPlaylistModel } from '../../models/video/video-playlist' 29import { VideoPlaylistModel } from '../../models/video/video-playlist'
30import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 30import {
31 commonVideoPlaylistFiltersValidator,
32 videoPlaylistsSearchValidator
33} from '../../middlewares/validators/videos/video-playlists'
31 34
32const accountsRouter = express.Router() 35const accountsRouter = express.Router()
33 36
@@ -72,6 +75,7 @@ accountsRouter.get('/:accountName/video-playlists',
72 setDefaultSort, 75 setDefaultSort,
73 setDefaultPagination, 76 setDefaultPagination,
74 commonVideoPlaylistFiltersValidator, 77 commonVideoPlaylistFiltersValidator,
78 videoPlaylistsSearchValidator,
75 asyncMiddleware(listAccountPlaylists) 79 asyncMiddleware(listAccountPlaylists)
76) 80)
77 81
@@ -135,6 +139,7 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
135 } 139 }
136 140
137 const resultList = await VideoPlaylistModel.listForApi({ 141 const resultList = await VideoPlaylistModel.listForApi({
142 search: req.query.search,
138 followerActorId: serverActor.id, 143 followerActorId: serverActor.id,
139 start: req.query.start, 144 start: req.query.start,
140 count: req.query.count, 145 count: req.query.count,
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 27ee62b1f..1d67e8666 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -166,6 +166,18 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
166 ] 166 ]
167} 167}
168 168
169const videoPlaylistsSearchValidator = [
170 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
171
172 (req: express.Request, res: express.Response, next: express.NextFunction) => {
173 logger.debug('Checking videoPlaylists search query', { parameters: req.query })
174
175 if (areValidationErrors(req, res)) return
176
177 return next()
178 }
179]
180
169const videoPlaylistsAddVideoValidator = [ 181const videoPlaylistsAddVideoValidator = [
170 param('playlistId') 182 param('playlistId')
171 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), 183 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
@@ -354,6 +366,7 @@ export {
354 videoPlaylistsUpdateValidator, 366 videoPlaylistsUpdateValidator,
355 videoPlaylistsDeleteValidator, 367 videoPlaylistsDeleteValidator,
356 videoPlaylistsGetValidator, 368 videoPlaylistsGetValidator,
369 videoPlaylistsSearchValidator,
357 370
358 videoPlaylistsAddVideoValidator, 371 videoPlaylistsAddVideoValidator,
359 videoPlaylistsUpdateOrRemoveVideoValidator, 372 videoPlaylistsUpdateOrRemoveVideoValidator,
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 278d80ac0..ef87a7ee9 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -13,10 +13,11 @@ import {
13 Model, 13 Model,
14 Scopes, 14 Scopes,
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt,
17 Sequelize
17} from 'sequelize-typescript' 18} from 'sequelize-typescript'
18import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 19import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
19import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils' 20import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid, createSimilarityAttribute } from '../utils'
20import { 21import {
21 isVideoPlaylistDescriptionValid, 22 isVideoPlaylistDescriptionValid,
22 isVideoPlaylistNameValid, 23 isVideoPlaylistNameValid,
@@ -67,7 +68,8 @@ type AvailableForListOptions = {
67 type?: VideoPlaylistType 68 type?: VideoPlaylistType
68 accountId?: number 69 accountId?: number
69 videoChannelId?: number 70 videoChannelId?: number
70 privateAndUnlisted?: boolean 71 privateAndUnlisted?: boolean,
72 search?: string
71} 73}
72 74
73@Scopes(() => ({ 75@Scopes(() => ({
@@ -163,6 +165,23 @@ type AvailableForListOptions = {
163 }) 165 })
164 } 166 }
165 167
168 if (options.search) {
169 const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
170 const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
171 whereAnd.push({
172 id: {
173 [ Op.in ]: Sequelize.literal(
174 '(' +
175 'SELECT "videoPlaylist"."id" FROM "videoPlaylist" ' +
176 'WHERE ' +
177 'lower(immutable_unaccent("videoPlaylist"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
178 'lower(immutable_unaccent("videoPlaylist"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
179 ')'
180 )
181 }
182 })
183 }
184
166 const where = { 185 const where = {
167 [Op.and]: whereAnd 186 [Op.and]: whereAnd
168 } 187 }
@@ -291,7 +310,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
291 type?: VideoPlaylistType, 310 type?: VideoPlaylistType,
292 accountId?: number, 311 accountId?: number,
293 videoChannelId?: number, 312 videoChannelId?: number,
294 privateAndUnlisted?: boolean 313 privateAndUnlisted?: boolean,
314 search?: string
295 }) { 315 }) {
296 const query = { 316 const query = {
297 offset: options.start, 317 offset: options.start,
@@ -308,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
308 followerActorId: options.followerActorId, 328 followerActorId: options.followerActorId,
309 accountId: options.accountId, 329 accountId: options.accountId,
310 videoChannelId: options.videoChannelId, 330 videoChannelId: options.videoChannelId,
311 privateAndUnlisted: options.privateAndUnlisted 331 privateAndUnlisted: options.privateAndUnlisted,
332 search: options.search
312 } as AvailableForListOptions 333 } as AvailableForListOptions
313 ] 334 ]
314 }, 335 },