Add channel filters for my videos/followers
This commit is contained in:
parent
7e76cc3800
commit
978c87e7f5
|
@ -28,12 +28,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { search: 'type:auto' },
|
||||
label: $localize`Automatic blocks`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'type:manual' },
|
||||
label: $localize`Manual blocks`
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
queryParams: { search: 'type:auto' },
|
||||
label: $localize`Automatic blocks`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'type:manual' },
|
||||
label: $localize`Manual blocks`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -44,12 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
|
|||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { search: 'local:true' },
|
||||
label: $localize`Local comments`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'local:false' },
|
||||
label: $localize`Remote comments`
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
queryParams: { search: 'local:true' },
|
||||
label: $localize`Local comments`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'local:false' },
|
||||
label: $localize`Remote comments`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -36,8 +36,13 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { search: 'banned:true' },
|
||||
label: $localize`Banned users`
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
queryParams: { search: 'banned:true' },
|
||||
label: $localize`Banned users`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -37,12 +37,19 @@ export class MyFollowersComponent implements OnInit {
|
|||
}
|
||||
|
||||
this.auth.userInformationLoaded.subscribe(() => {
|
||||
this.inputFilters = this.auth.getUser().videoChannels.map(c => {
|
||||
const channelFilters = this.auth.getUser().videoChannels.map(c => {
|
||||
return {
|
||||
queryParams: { search: 'channel:' + c.name },
|
||||
label: $localize`Followers of ${c.name}`
|
||||
label: c.name
|
||||
}
|
||||
})
|
||||
|
||||
this.inputFilters = [
|
||||
{
|
||||
title: $localize`Channel filters`,
|
||||
children: channelFilters
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
|||
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
||||
import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
|
||||
import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
|
||||
import { VideoSortField } from '@shared/models'
|
||||
import { VideoChannel, VideoSortField } from '@shared/models'
|
||||
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
|
||||
|
||||
@Component({
|
||||
|
@ -47,16 +47,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
|
||||
user: User
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { search: 'isLive:true' },
|
||||
label: $localize`Only live videos`
|
||||
}
|
||||
]
|
||||
inputFilters: AdvancedInputFilter[]
|
||||
|
||||
disabled = false
|
||||
|
||||
private search: string
|
||||
private userChannels: VideoChannel[] = []
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
|
@ -79,6 +75,35 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
if (this.route.snapshot.queryParams['search']) {
|
||||
this.search = this.route.snapshot.queryParams['search']
|
||||
}
|
||||
|
||||
this.authService.userInformationLoaded.subscribe(() => {
|
||||
this.user = this.authService.getUser()
|
||||
this.userChannels = this.user.videoChannels
|
||||
|
||||
const channelFilters = this.userChannels.map(c => {
|
||||
return {
|
||||
queryParams: { search: 'channel:' + c.name },
|
||||
label: c.name
|
||||
}
|
||||
})
|
||||
|
||||
this.inputFilters = [
|
||||
{
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
queryParams: { search: 'isLive:true' },
|
||||
label: $localize`Only live videos`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: $localize`Channel filters`,
|
||||
children: channelFilters
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
onSearch (search: string) {
|
||||
|
@ -105,7 +130,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
getVideosObservable (page: number) {
|
||||
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||
|
||||
return this.videoService.getMyVideos(newPagination, this.sort, this.search)
|
||||
return this.videoService.getMyVideos({
|
||||
videoPagination: newPagination,
|
||||
sort: this.sort,
|
||||
userChannels: this.userChannels,
|
||||
search: this.search
|
||||
})
|
||||
.pipe(
|
||||
tap(res => this.pagination.totalItems = res.total)
|
||||
)
|
||||
|
|
|
@ -39,24 +39,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
|
|||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { search: 'state:pending' },
|
||||
label: $localize`Unsolved reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'state:accepted' },
|
||||
label: $localize`Accepted reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'state:rejected' },
|
||||
label: $localize`Refused reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'videoIs:blacklisted' },
|
||||
label: $localize`Reports with blocked videos`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'videoIs:deleted' },
|
||||
label: $localize`Reports with deleted videos`
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
queryParams: { search: 'state:pending' },
|
||||
label: $localize`Unsolved reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'state:accepted' },
|
||||
label: $localize`Accepted reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'state:rejected' },
|
||||
label: $localize`Refused reports`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'videoIs:blacklisted' },
|
||||
label: $localize`Reports with blocked videos`
|
||||
},
|
||||
{
|
||||
queryParams: { search: 'videoIs:deleted' },
|
||||
label: $localize`Reports with deleted videos`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
</div>
|
||||
|
||||
<div role="menu" ngbDropdownMenu>
|
||||
<h6 class="dropdown-header" i18n>Advanced filters</h6>
|
||||
<ng-container *ngFor="let group of filters">
|
||||
<h6 class="dropdown-header">{{ group.title }}</h6>
|
||||
|
||||
<a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
|
||||
{{ filter.label }}
|
||||
</a>
|
||||
<a *ngFor="let filter of group.children" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
|
||||
{{ filter.label }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,8 +5,12 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@
|
|||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
|
||||
export type AdvancedInputFilter = {
|
||||
label: string
|
||||
queryParams: Params
|
||||
title: string
|
||||
|
||||
children: {
|
||||
label: string
|
||||
queryParams: Params
|
||||
}[]
|
||||
}
|
||||
|
||||
const logger = debug('peertube:AdvancedInputFilterComponent')
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
UserVideoRateType,
|
||||
UserVideoRateUpdate,
|
||||
Video as VideoServerModel,
|
||||
VideoChannel as VideoChannelServerModel,
|
||||
VideoConstant,
|
||||
VideoDetails as VideoDetailsServerModel,
|
||||
VideoFileMetadata,
|
||||
|
@ -122,7 +123,14 @@ export class VideoService {
|
|||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable<ResultList<Video>> {
|
||||
getMyVideos (options: {
|
||||
videoPagination: ComponentPaginationLight
|
||||
sort: VideoSortField
|
||||
userChannels?: VideoChannelServerModel[]
|
||||
search?: string
|
||||
}): Observable<ResultList<Video>> {
|
||||
const { videoPagination, sort, userChannels = [], search } = options
|
||||
|
||||
const pagination = this.restService.componentToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -133,6 +141,16 @@ export class VideoService {
|
|||
isLive: {
|
||||
prefix: 'isLive:',
|
||||
isBoolean: true
|
||||
},
|
||||
channelId: {
|
||||
prefix: 'channel:',
|
||||
handler: (name: string) => {
|
||||
const channel = userChannels.find(c => c.name === name)
|
||||
|
||||
if (channel) return channel.id
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
usersUpdateMeValidator,
|
||||
usersVideoRatingValidator
|
||||
} from '../../../middlewares'
|
||||
import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
|
||||
import { deleteMeValidator, usersVideosValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
|
||||
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
|
||||
import { AccountModel } from '../../../models/account/account'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
||||
|
@ -69,6 +69,7 @@ meRouter.get('/me/videos',
|
|||
videosSortValidator,
|
||||
setDefaultVideosSort,
|
||||
setDefaultPagination,
|
||||
asyncMiddleware(usersVideosValidator),
|
||||
asyncMiddleware(getUserVideos)
|
||||
)
|
||||
|
||||
|
@ -113,6 +114,7 @@ async function getUserVideos (req: express.Request, res: express.Response) {
|
|||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
search: req.query.search,
|
||||
channelId: res.locals.videoChannel?.id,
|
||||
isLive: req.query.isLive
|
||||
}, 'filter:api.user.me.videos.list.params')
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { omit } from 'lodash'
|
|||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { MUserDefault } from '@server/types/models'
|
||||
import { HttpStatusCode, UserRegister, UserRole } from '@shared/models'
|
||||
import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
|
||||
import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
|
||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
|
||||
import {
|
||||
isUserAdminFlagsValid,
|
||||
|
@ -31,7 +31,7 @@ import { Redis } from '../../lib/redis'
|
|||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
|
||||
import { ActorModel } from '../../models/actor/actor'
|
||||
import { UserModel } from '../../models/user/user'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
|
||||
import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
|
||||
|
||||
const usersListValidator = [
|
||||
query('blocked')
|
||||
|
@ -318,6 +318,28 @@ const usersVideoRatingValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const usersVideosValidator = [
|
||||
query('isLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid live boolean'),
|
||||
|
||||
query('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid).withMessage('Should have a valid channel id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking usersVideosValidator parameters', { parameters: req.params })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const ensureUserRegistrationAllowed = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const allowedParams = {
|
||||
|
@ -513,6 +535,7 @@ export {
|
|||
ensureUserRegistrationAllowed,
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
usersGetValidator,
|
||||
usersVideosValidator,
|
||||
usersAskResetPasswordValidator,
|
||||
usersResetPasswordValidator,
|
||||
usersAskSendVerifyEmailValidator,
|
||||
|
|
|
@ -978,10 +978,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
|
||||
channelId?: number
|
||||
isLive?: boolean
|
||||
search?: string
|
||||
}) {
|
||||
const { accountId, start, count, sort, search, isLive } = options
|
||||
const { accountId, channelId, start, count, sort, search, isLive } = options
|
||||
|
||||
function buildBaseQuery (): FindOptions {
|
||||
const where: WhereOptions = {}
|
||||
|
@ -996,6 +998,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
where.isLive = isLive
|
||||
}
|
||||
|
||||
const channelWhere = channelId
|
||||
? { id: channelId }
|
||||
: {}
|
||||
|
||||
const baseQuery = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
|
@ -1005,6 +1011,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
|||
{
|
||||
model: VideoChannelModel,
|
||||
required: true,
|
||||
where: channelWhere,
|
||||
include: [
|
||||
{
|
||||
model: AccountModel,
|
||||
|
|
|
@ -119,6 +119,20 @@ describe('Test videos API validator', function () {
|
|||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid channel', async function () {
|
||||
await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown channel', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { channelId: 89898 },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
|
|
@ -318,6 +318,8 @@ describe('Test users', function () {
|
|||
fixture: 'video_short.webm'
|
||||
}
|
||||
await server.videos.upload({ token: userToken, attributes })
|
||||
|
||||
await server.channels.create({ token: userToken, attributes: { name: 'other_channel' } })
|
||||
})
|
||||
|
||||
it('Should have video quota updated', async function () {
|
||||
|
@ -340,6 +342,29 @@ describe('Test users', function () {
|
|||
expect(video.previewPath).to.not.be.null
|
||||
})
|
||||
|
||||
it('Should be able to filter by channel in my videos', async function () {
|
||||
const myInfo = await server.users.getMyInfo({ token: userToken })
|
||||
const mainChannel = myInfo.videoChannels.find(c => c.name !== 'other_channel')
|
||||
const otherChannel = myInfo.videoChannels.find(c => c.name === 'other_channel')
|
||||
|
||||
{
|
||||
const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: mainChannel.id })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const video: Video = data[0]
|
||||
expect(video.name).to.equal('super user video')
|
||||
expect(video.thumbnailPath).to.not.be.null
|
||||
expect(video.previewPath).to.not.be.null
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: otherChannel.id })
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should be able to search in my videos', async function () {
|
||||
{
|
||||
const { total, data } = await server.videos.listMyVideos({ token: userToken, sort: '-createdAt', search: 'user video' })
|
||||
|
|
|
@ -207,6 +207,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
sort?: string
|
||||
search?: string
|
||||
isLive?: boolean
|
||||
channelId?: number
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/me/videos'
|
||||
|
||||
|
@ -214,7 +215,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue