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) {
overflow-y: auto;
flex-grow: 1;
}
}

View file

@ -41,6 +41,10 @@
</div>
</div>
<div class="input-container">
<input type="text" placeholder="Search playlists" [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
</div>
<div class="playlists">
<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>

View file

@ -53,6 +53,15 @@
overflow-y: auto;
}
.input-container {
display: flex;
input {
flex-grow: 1;
margin: 0 15px 10px 15px;
}
}
.playlist {
display: flex;
cursor: pointer;
@ -76,7 +85,6 @@
.new-playlist-button,
.new-playlist-block {
padding-top: 10px;
margin-top: 10px;
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 { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
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 { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms'
import { I18n } from '@ngx-translate/i18n-polyfill'
@ -29,6 +30,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
@Input() lazyLoad = false
isNewPlaylistBlockOpened = false
videoPlaylistSearch: string
videoPlaylistSearchChanged = new Subject<string>()
videoPlaylists: PlaylistSummary[] = []
timestampOptions: {
startTimestampEnabled: boolean
@ -58,6 +61,13 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
this.buildForm({
displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
})
this.videoPlaylistSearchChanged
.pipe(
debounceTime(500))
.subscribe(() => {
this.load()
})
}
ngOnChanges (simpleChanges: SimpleChanges) {
@ -74,6 +84,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
reload () {
this.videoPlaylists = []
this.videoPlaylistSearch = undefined
this.init()
@ -82,11 +93,12 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
load () {
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)
])
.subscribe(
([ playlistsResult, existResult ]) => {
this.videoPlaylists = []
for (const playlist of playlistsResult.data) {
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})`
}
onVideoPlaylistSearchChanged () {
this.videoPlaylistSearchChanged.next()
}
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
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 pagination = componentPagination
? this.restService.componentPaginationToRestPagination(componentPagination)
@ -67,6 +72,7 @@ export class VideoPlaylistService {
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
if (search) params = this.restService.addObjectParams(params, { search })
return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params })
.pipe(
@ -213,8 +219,8 @@ export class VideoPlaylistService {
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
let params = new HttpParams()
let params = new HttpParams()
params = this.restService.addObjectParams(params, { videoIds })
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 { logger } from '../../helpers/logger'
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()
@ -72,6 +75,7 @@ accountsRouter.get('/:accountName/video-playlists',
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
videoPlaylistsSearchValidator,
asyncMiddleware(listAccountPlaylists)
)
@ -135,6 +139,7 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
}
const resultList = await VideoPlaylistModel.listForApi({
search: req.query.search,
followerActorId: serverActor.id,
start: req.query.start,
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 = [
param('playlistId')
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
@ -354,6 +366,7 @@ export {
videoPlaylistsUpdateValidator,
videoPlaylistsDeleteValidator,
videoPlaylistsGetValidator,
videoPlaylistsSearchValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsUpdateOrRemoveVideoValidator,

View file

@ -13,10 +13,11 @@ import {
Model,
Scopes,
Table,
UpdatedAt
UpdatedAt,
Sequelize
} from 'sequelize-typescript'
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 {
isVideoPlaylistDescriptionValid,
isVideoPlaylistNameValid,
@ -67,7 +68,8 @@ type AvailableForListOptions = {
type?: VideoPlaylistType
accountId?: number
videoChannelId?: number
privateAndUnlisted?: boolean
privateAndUnlisted?: boolean,
search?: string
}
@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 = {
[Op.and]: whereAnd
}
@ -291,7 +310,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
type?: VideoPlaylistType,
accountId?: number,
videoChannelId?: number,
privateAndUnlisted?: boolean
privateAndUnlisted?: boolean,
search?: string
}) {
const query = {
offset: options.start,
@ -308,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
followerActorId: options.followerActorId,
accountId: options.accountId,
videoChannelId: options.videoChannelId,
privateAndUnlisted: options.privateAndUnlisted
privateAndUnlisted: options.privateAndUnlisted,
search: options.search
} as AvailableForListOptions
]
},