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 @@
& > my-user-notifications:nth-child(2) {
overflow-y: auto;
+ flex-grow: 1;
}
}
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 @@
+
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 @@
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;
}
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 @@
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()
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
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 {
)
}
- listAccountPlaylists (account: Account, componentPagination: ComponentPagination, sort: string): Observable> {
+ listAccountPlaylists (
+ account: Account,
+ componentPagination: ComponentPagination,
+ sort: string,
+ search?: string
+ ): Observable> {
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>(url, { params })
.pipe(
@@ -213,8 +219,8 @@ export class VideoPlaylistService {
private doVideosExistInPlaylist (videoIds: number[]): Observable {
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(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'
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,
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) => {
]
}
+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,
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 {
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 {
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 {
followerActorId: options.followerActorId,
accountId: options.accountId,
videoChannelId: options.videoChannelId,
- privateAndUnlisted: options.privateAndUnlisted
+ privateAndUnlisted: options.privateAndUnlisted,
+ search: options.search
} as AvailableForListOptions
]
},