1
0
Fork 0

Add playlist search option and search input for add-to-video-playlist dropdown

fixes #2138
This commit is contained in:
Rigel Kent 2019-12-26 11:52:46 +01:00
parent def2a70b7e
commit c06af5012e
No known key found for this signature in database
GPG key ID: 5E53E96A494E452F
8 changed files with 85 additions and 11 deletions

View file

@ -34,6 +34,7 @@
& > my-user-notifications:nth-child(2) { & > my-user-notifications:nth-child(2) {
overflow-y: auto; overflow-y: auto;
flex-grow: 1;
} }
} }

View file

@ -41,6 +41,10 @@
</div> </div>
</div> </div>
<div class="input-container">
<input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
</div>
<div class="playlists"> <div class="playlists">
<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
<my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox>

View file

@ -53,6 +53,15 @@
overflow-y: auto; overflow-y: auto;
} }
.input-container {
display: flex;
input {
flex-grow: 1;
margin: 0 15px 10px 15px;
}
}
.playlist { .playlist {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
@ -76,7 +85,6 @@
.new-playlist-button, .new-playlist-button,
.new-playlist-block { .new-playlist-block {
padding-top: 10px; padding-top: 10px;
margin-top: 10px;
border-top: 1px solid $separator-border-color; border-top: 1px solid $separator-border-color;
} }

View file

@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { AuthService, Notifier } from '@app/core' import { AuthService, Notifier } from '@app/core'
import { forkJoin } from 'rxjs' import { forkJoin, Subject } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models'
import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms' import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
@ -29,6 +30,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
@Input() lazyLoad = false @Input() lazyLoad = false
isNewPlaylistBlockOpened = false isNewPlaylistBlockOpened = false
videoPlaylistSearch: string
videoPlaylistSearchChanged = new Subject<string>()
videoPlaylists: PlaylistSummary[] = [] videoPlaylists: PlaylistSummary[] = []
timestampOptions: { timestampOptions: {
startTimestampEnabled: boolean startTimestampEnabled: boolean
@ -58,6 +61,13 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
this.buildForm({ this.buildForm({
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
}) })
this.videoPlaylistSearchChanged
.pipe(
debounceTime(500))
.subscribe(() => {
this.load()
})
} }
ngOnChanges (simpleChanges: SimpleChanges) { ngOnChanges (simpleChanges: SimpleChanges) {
@ -74,6 +84,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
reload () { reload () {
this.videoPlaylists = [] this.videoPlaylists = []
this.videoPlaylistSearch = undefined
this.init() this.init()
@ -82,11 +93,12 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
load () { load () {
forkJoin([ forkJoin([
this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined,'-updatedAt'), this.videoPlaylistService.listAccountPlaylists(this.user.account, undefined, '-updatedAt', this.videoPlaylistSearch),
this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
]) ])
.subscribe( .subscribe(
([ playlistsResult, existResult ]) => { ([ playlistsResult, existResult ]) => {
this.videoPlaylists = []
for (const playlist of playlistsResult.data) { for (const playlist of playlistsResult.data) {
const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id) const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id)
@ -178,6 +190,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
return `(${start}-${stop})` return `(${start}-${stop})`
} }
onVideoPlaylistSearchChanged () {
this.videoPlaylistSearchChanged.next()
}
private removeVideoFromPlaylist (playlist: PlaylistSummary) { private removeVideoFromPlaylist (playlist: PlaylistSummary) {
if (!playlist.playlistElementId) return if (!playlist.playlistElementId) return

View file

@ -59,7 +59,12 @@ export class VideoPlaylistService {
) )
} }
listAccountPlaylists (account: Account, componentPagination: ComponentPagination, sort: string): Observable<ResultList<VideoPlaylist>> { listAccountPlaylists (
account: Account,
componentPagination: ComponentPagination,
sort: string,
search?: string
): Observable<ResultList<VideoPlaylist>> {
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
const pagination = componentPagination const pagination = componentPagination
? this.restService.componentPaginationToRestPagination(componentPagination) ? this.restService.componentPaginationToRestPagination(componentPagination)
@ -67,6 +72,7 @@ export class VideoPlaylistService {
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) params = this.restService.addRestGetParams(params, pagination, sort)
if (search) params = this.restService.addObjectParams(params, { search })
return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
.pipe( .pipe(
@ -213,8 +219,8 @@ export class VideoPlaylistService {
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
let params = new HttpParams()
let params = new HttpParams()
params = this.restService.addObjectParams(params, { videoIds }) params = this.restService.addObjectParams(params, { videoIds })
return this.authHttp.get<VideoExistInPlaylist>(url, { params }) return this.authHttp.get<VideoExistInPlaylist>(url, { params })

View file

@ -27,7 +27,10 @@ import { VideoChannelModel } from '../../models/video/video-channel'
import { JobQueue } from '../../lib/job-queue' import { JobQueue } from '../../lib/job-queue'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { VideoPlaylistModel } from '../../models/video/video-playlist' import { VideoPlaylistModel } from '../../models/video/video-playlist'
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' import {
commonVideoPlaylistFiltersValidator,
videoPlaylistsSearchValidator
} from '../../middlewares/validators/videos/video-playlists'
const accountsRouter = express.Router() const accountsRouter = express.Router()
@ -72,6 +75,7 @@ accountsRouter.get('/:accountName/video-playlists',
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
commonVideoPlaylistFiltersValidator, commonVideoPlaylistFiltersValidator,
videoPlaylistsSearchValidator,
asyncMiddleware(listAccountPlaylists) asyncMiddleware(listAccountPlaylists)
) )
@ -135,6 +139,7 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
} }
const resultList = await VideoPlaylistModel.listForApi({ const resultList = await VideoPlaylistModel.listForApi({
search: req.query.search,
followerActorId: serverActor.id, followerActorId: serverActor.id,
start: req.query.start, start: req.query.start,
count: req.query.count, count: req.query.count,

View file

@ -166,6 +166,18 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
] ]
} }
const videoPlaylistsSearchValidator = [
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoPlaylists search query', { parameters: req.query })
if (areValidationErrors(req, res)) return
return next()
}
]
const videoPlaylistsAddVideoValidator = [ const videoPlaylistsAddVideoValidator = [
param('playlistId') param('playlistId')
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
@ -354,6 +366,7 @@ export {
videoPlaylistsUpdateValidator, videoPlaylistsUpdateValidator,
videoPlaylistsDeleteValidator, videoPlaylistsDeleteValidator,
videoPlaylistsGetValidator, videoPlaylistsGetValidator,
videoPlaylistsSearchValidator,
videoPlaylistsAddVideoValidator, videoPlaylistsAddVideoValidator,
videoPlaylistsUpdateOrRemoveVideoValidator, videoPlaylistsUpdateOrRemoveVideoValidator,

View file

@ -13,10 +13,11 @@ import {
Model, Model,
Scopes, Scopes,
Table, Table,
UpdatedAt UpdatedAt,
Sequelize
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils' import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid, createSimilarityAttribute } from '../utils'
import { import {
isVideoPlaylistDescriptionValid, isVideoPlaylistDescriptionValid,
isVideoPlaylistNameValid, isVideoPlaylistNameValid,
@ -67,7 +68,8 @@ type AvailableForListOptions = {
type?: VideoPlaylistType type?: VideoPlaylistType
accountId?: number accountId?: number
videoChannelId?: number videoChannelId?: number
privateAndUnlisted?: boolean privateAndUnlisted?: boolean,
search?: string
} }
@Scopes(() => ({ @Scopes(() => ({
@ -163,6 +165,23 @@ type AvailableForListOptions = {
}) })
} }
if (options.search) {
const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
whereAnd.push({
id: {
[ Op.in ]: Sequelize.literal(
'(' +
'SELECT "videoPlaylist"."id" FROM "videoPlaylist" ' +
'WHERE ' +
'lower(immutable_unaccent("videoPlaylist"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
'lower(immutable_unaccent("videoPlaylist"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
')'
)
}
})
}
const where = { const where = {
[Op.and]: whereAnd [Op.and]: whereAnd
} }
@ -291,7 +310,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
type?: VideoPlaylistType, type?: VideoPlaylistType,
accountId?: number, accountId?: number,
videoChannelId?: number, videoChannelId?: number,
privateAndUnlisted?: boolean privateAndUnlisted?: boolean,
search?: string
}) { }) {
const query = { const query = {
offset: options.start, offset: options.start,
@ -308,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
followerActorId: options.followerActorId, followerActorId: options.followerActorId,
accountId: options.accountId, accountId: options.accountId,
videoChannelId: options.videoChannelId, videoChannelId: options.videoChannelId,
privateAndUnlisted: options.privateAndUnlisted privateAndUnlisted: options.privateAndUnlisted,
search: options.search
} as AvailableForListOptions } as AvailableForListOptions
] ]
}, },