harmonize search for libraries
This commit is contained in:
parent
bc99dfe54e
commit
4f5d045960
|
@ -112,8 +112,10 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email">
|
<td *ngIf="getColumn('email')" [title]="user.email">
|
||||||
<a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
|
<ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">
|
||||||
|
<a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
|
||||||
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<ng-template #emailWithVerificationStatus>
|
<ng-template #emailWithVerificationStatus>
|
||||||
|
|
|
@ -7,6 +7,13 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { ServerConfig, User, UserRole } from '@shared/models'
|
import { ServerConfig, User, UserRole } from '@shared/models'
|
||||||
import { Params, Router, ActivatedRoute } from '@angular/router'
|
import { Params, Router, ActivatedRoute } from '@angular/router'
|
||||||
|
|
||||||
|
type UserForList = User & {
|
||||||
|
rawVideoQuota: number
|
||||||
|
rawVideoQuotaUsed: number
|
||||||
|
rawVideoQuotaDaily: number
|
||||||
|
rawVideoQuotaUsedDaily: number
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-user-list',
|
selector: 'my-user-list',
|
||||||
templateUrl: './user-list.component.html',
|
templateUrl: './user-list.component.html',
|
||||||
|
@ -24,8 +31,8 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
selectedUsers: User[] = []
|
selectedUsers: User[] = []
|
||||||
bulkUserActions: DropdownAction<User[]>[][] = []
|
bulkUserActions: DropdownAction<User[]>[][] = []
|
||||||
columns: { key: string, label: string }[]
|
columns: { key: string, label: string }[]
|
||||||
_selectedColumns: { key: string, label: string }[]
|
|
||||||
|
|
||||||
|
private _selectedColumns: { key: string, label: string }[]
|
||||||
private serverConfig: ServerConfig
|
private serverConfig: ServerConfig
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -111,7 +118,7 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
{ key: 'role', label: 'Role' },
|
{ key: 'role', label: 'Role' },
|
||||||
{ key: 'createdAt', label: 'Created' }
|
{ key: 'createdAt', label: 'Created' }
|
||||||
]
|
]
|
||||||
this.selectedColumns = [...this.columns]
|
this.selectedColumns = [ ...this.columns ] // make a full copy of the array
|
||||||
this.columns.push({ key: 'quotaDaily', label: 'Daily quota' })
|
this.columns.push({ key: 'quotaDaily', label: 'Daily quota' })
|
||||||
this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' })
|
this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' })
|
||||||
this.columns.push({ key: 'lastLoginDate', label: 'Last login' })
|
this.columns.push({ key: 'lastLoginDate', label: 'Last login' })
|
||||||
|
@ -133,14 +140,14 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumn (key: string) {
|
getColumn (key: string) {
|
||||||
return this.selectedColumns.find((col: any) => col.key === key)
|
return this.selectedColumns.find((col: { key: string }) => col.key === key)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) {
|
getUserVideoQuotaPercentage (user: UserForList) {
|
||||||
return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota
|
return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) {
|
getUserVideoQuotaDailyPercentage (user: UserForList) {
|
||||||
return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily
|
return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
<h1>
|
<h1 class="d-flex justify-content-between">
|
||||||
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My channels</ng-container>
|
<my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
|
||||||
</h1>
|
<ng-container i18n>My channels</ng-container>
|
||||||
|
<span class="badge badge-secondary">{{ totalItems }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="has-feedback has-clear">
|
||||||
|
<input type="text" placeholder="Search your channels" i18n-placeholder [(ngModel)]="channelsSearch" (ngModelChange)="onChannelsSearchChanged()" />
|
||||||
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="video-channels-header">
|
|
||||||
<a class="create-button" routerLink="create">
|
<a class="create-button" routerLink="create">
|
||||||
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
|
||||||
<ng-container i18n>Create video channel</ng-container>
|
<ng-container i18n>Create video channel</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</h1>
|
||||||
|
|
||||||
<div class="video-channels">
|
<div class="video-channels">
|
||||||
<div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
|
<div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
@include create-button;
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
@include peertube-input-text(300px);
|
||||||
|
}
|
||||||
|
|
||||||
::ng-deep .action-button {
|
::ng-deep .action-button {
|
||||||
&.action-button-edit {
|
&.action-button-edit {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -55,11 +59,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-channels-header {
|
|
||||||
text-align: right;
|
|
||||||
margin: 20px 0 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .chartjs-render-monitor {
|
::ng-deep .chartjs-render-monitor {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { ChartData } from 'chart.js'
|
import { ChartData } from 'chart.js'
|
||||||
import { max, maxBy, min, minBy } from 'lodash-es'
|
import { max, maxBy, min, minBy } from 'lodash-es'
|
||||||
import { flatMap } from 'rxjs/operators'
|
import { flatMap, debounceTime } from 'rxjs/operators'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
|
import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
|
||||||
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-video-channels',
|
selector: 'my-account-video-channels',
|
||||||
|
@ -12,11 +13,16 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
styleUrls: [ './my-account-video-channels.component.scss' ]
|
styleUrls: [ './my-account-video-channels.component.scss' ]
|
||||||
})
|
})
|
||||||
export class MyAccountVideoChannelsComponent implements OnInit {
|
export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
|
totalItems: number
|
||||||
|
|
||||||
videoChannels: VideoChannel[] = []
|
videoChannels: VideoChannel[] = []
|
||||||
videoChannelsChartData: ChartData[]
|
videoChannelsChartData: ChartData[]
|
||||||
videoChannelsMinimumDailyViews = 0
|
videoChannelsMinimumDailyViews = 0
|
||||||
videoChannelsMaximumDailyViews: number
|
videoChannelsMaximumDailyViews: number
|
||||||
|
|
||||||
|
channelsSearch: string
|
||||||
|
channelsSearchChanged = new Subject<string>()
|
||||||
|
|
||||||
private user: User
|
private user: User
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -32,6 +38,12 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
this.user = this.authService.getUser()
|
this.user = this.authService.getUser()
|
||||||
|
|
||||||
this.loadVideoChannels()
|
this.loadVideoChannels()
|
||||||
|
|
||||||
|
this.channelsSearchChanged
|
||||||
|
.pipe(debounceTime(500))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.loadVideoChannels()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInSmallView () {
|
get isInSmallView () {
|
||||||
|
@ -87,6 +99,15 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.channelsSearch = ''
|
||||||
|
this.onChannelsSearchChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
onChannelsSearchChanged () {
|
||||||
|
this.channelsSearchChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
async deleteVideoChannel (videoChannel: VideoChannel) {
|
async deleteVideoChannel (videoChannel: VideoChannel) {
|
||||||
const res = await this.confirmService.confirmWithInput(
|
const res = await this.confirmService.confirmWithInput(
|
||||||
this.i18n(
|
this.i18n(
|
||||||
|
@ -118,9 +139,10 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
|
|
||||||
private loadVideoChannels () {
|
private loadVideoChannels () {
|
||||||
this.authService.userInformationLoaded
|
this.authService.userInformationLoaded
|
||||||
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true)))
|
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch)))
|
||||||
.subscribe(res => {
|
.subscribe(res => {
|
||||||
this.videoChannels = res.data
|
this.videoChannels = res.data
|
||||||
|
this.totalItems = res.total
|
||||||
|
|
||||||
// chart data
|
// chart data
|
||||||
this.videoChannelsChartData = this.videoChannels.map(v => ({
|
this.videoChannelsChartData = this.videoChannels.map(v => ({
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="peertube-select-container peertube-select-button ml-2">
|
<div class="peertube-select-container peertube-select-button ml-2">
|
||||||
<select [(ngModel)]="notificationSortType" class="form-control">
|
<select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control">
|
||||||
<option value="undefined" disabled>Sort by</option>
|
<option value="undefined" disabled>Sort by</option>
|
||||||
<option value="created" i18n>Newest first</option>
|
<option value="createdAt" i18n>Newest first</option>
|
||||||
<option value="unread-created" i18n>Unread first</option>
|
<option value="read" [disabled]="!hasUnreadNotifications()" i18n>Unread first</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { Component, ViewChild } from '@angular/core'
|
import { Component, ViewChild } from '@angular/core'
|
||||||
import { UserNotificationsComponent } from '@app/shared/shared-main'
|
import { UserNotificationsComponent } from '@app/shared/shared-main'
|
||||||
|
|
||||||
|
type NotificationSortType = 'createdAt' | 'read'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './my-account-notifications.component.html',
|
templateUrl: './my-account-notifications.component.html',
|
||||||
styleUrls: [ './my-account-notifications.component.scss' ]
|
styleUrls: [ './my-account-notifications.component.scss' ]
|
||||||
|
@ -8,7 +10,17 @@ import { UserNotificationsComponent } from '@app/shared/shared-main'
|
||||||
export class MyAccountNotificationsComponent {
|
export class MyAccountNotificationsComponent {
|
||||||
@ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent
|
@ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent
|
||||||
|
|
||||||
notificationSortType = 'created'
|
_notificationSortType: NotificationSortType = 'createdAt'
|
||||||
|
|
||||||
|
get notificationSortType () {
|
||||||
|
return !this.hasUnreadNotifications()
|
||||||
|
? 'createdAt'
|
||||||
|
: this._notificationSortType
|
||||||
|
}
|
||||||
|
|
||||||
|
set notificationSortType (type: NotificationSortType) {
|
||||||
|
this._notificationSortType = type
|
||||||
|
}
|
||||||
|
|
||||||
markAllAsRead () {
|
markAllAsRead () {
|
||||||
this.userNotification.markAllAsRead()
|
this.userNotification.markAllAsRead()
|
||||||
|
@ -17,4 +29,8 @@ export class MyAccountNotificationsComponent {
|
||||||
hasUnreadNotifications () {
|
hasUnreadNotifications () {
|
||||||
return this.userNotification.notifications.filter(n => n.read === false).length !== 0
|
return this.userNotification.notifications.filter(n => n.read === false).length !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeSortColumn () {
|
||||||
|
this.userNotification.changeSortColumn(this.notificationSortType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,11 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td>
|
<td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td>
|
||||||
<td i18n>{{ videoChangeOwnership.status }}</td>
|
|
||||||
|
<td>
|
||||||
|
<span class="badge" [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
||||||
<my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
<my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
@include chip;
|
@include chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
@include table-badge;
|
||||||
|
}
|
||||||
|
|
||||||
.video-table-video {
|
.video-table-video {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api'
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { Notifier, RestPagination, RestTable } from '@app/core'
|
import { Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main'
|
import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main'
|
||||||
import { VideoChangeOwnership } from '@shared/models'
|
import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models'
|
||||||
import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
|
import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
|
||||||
import { getAbsoluteAPIUrl } from '@app/helpers'
|
import { getAbsoluteAPIUrl } from '@app/helpers'
|
||||||
|
|
||||||
|
@ -34,6 +34,17 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
|
||||||
return 'MyAccountOwnershipComponent'
|
return 'MyAccountOwnershipComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStatusClass (status: VideoChangeOwnershipStatus) {
|
||||||
|
switch (status) {
|
||||||
|
case VideoChangeOwnershipStatus.ACCEPTED:
|
||||||
|
return 'badge-green'
|
||||||
|
case VideoChangeOwnershipStatus.REFUSED:
|
||||||
|
return 'badge-red'
|
||||||
|
default:
|
||||||
|
return 'badge-yellow'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switchToDefaultAvatar ($event: Event) {
|
switchToDefaultAvatar ($event: Event) {
|
||||||
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
<h1>
|
<h1 class="d-flex justify-content-between">
|
||||||
<my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My subscriptions</ng-container>
|
<my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>My subscriptions</ng-container>
|
||||||
|
<span class="badge badge-secondary"> {{ pagination.totalItems }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="has-feedback has-clear">
|
||||||
|
<input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" (ngModelChange)="onSubscriptionsSearchChanged()" />
|
||||||
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div>
|
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
@include peertube-input-text(300px);
|
||||||
|
}
|
||||||
|
|
||||||
.video-channel {
|
.video-channel {
|
||||||
@include row-blocks;
|
@include row-blocks;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core'
|
||||||
import { ComponentPagination, Notifier } from '@app/core'
|
import { ComponentPagination, Notifier } from '@app/core'
|
||||||
import { VideoChannel } from '@app/shared/shared-main'
|
import { VideoChannel } from '@app/shared/shared-main'
|
||||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
|
import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
|
||||||
|
import { debounceTime } from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-subscriptions',
|
selector: 'my-account-subscriptions',
|
||||||
|
@ -20,6 +21,9 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
||||||
|
|
||||||
onDataSubject = new Subject<any[]>()
|
onDataSubject = new Subject<any[]>()
|
||||||
|
|
||||||
|
subscriptionsSearch: string
|
||||||
|
subscriptionsSearchChanged = new Subject<string>()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private userSubscriptionService: UserSubscriptionService,
|
private userSubscriptionService: UserSubscriptionService,
|
||||||
private notifier: Notifier
|
private notifier: Notifier
|
||||||
|
@ -27,20 +31,22 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.loadSubscriptions()
|
this.loadSubscriptions()
|
||||||
|
|
||||||
|
this.subscriptionsSearchChanged
|
||||||
|
.pipe(debounceTime(500))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.pagination.currentPage = 1
|
||||||
|
this.loadSubscriptions(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSubscriptions () {
|
resetSearch () {
|
||||||
this.userSubscriptionService.listSubscriptions(this.pagination)
|
this.subscriptionsSearch = ''
|
||||||
.subscribe(
|
this.onSubscriptionsSearchChanged()
|
||||||
res => {
|
}
|
||||||
this.videoChannels = this.videoChannels.concat(res.data)
|
|
||||||
this.pagination.totalItems = res.total
|
|
||||||
|
|
||||||
this.onDataSubject.next(res.data)
|
onSubscriptionsSearchChanged () {
|
||||||
},
|
this.subscriptionsSearchChanged.next()
|
||||||
|
|
||||||
error => this.notifier.error(error.message)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNearOfBottom () {
|
onNearOfBottom () {
|
||||||
|
@ -51,4 +57,19 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
||||||
this.loadSubscriptions()
|
this.loadSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadSubscriptions (more = true) {
|
||||||
|
this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch })
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
this.videoChannels = more
|
||||||
|
? this.videoChannels.concat(res.data)
|
||||||
|
: res.data
|
||||||
|
this.pagination.totalItems = res.total
|
||||||
|
|
||||||
|
this.onDataSubject.next(res.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
error => this.notifier.error(error.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,12 @@
|
||||||
<ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container>
|
<ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ videoImport.state.label }}</td>
|
<td>
|
||||||
|
<span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state)">
|
||||||
|
{{ videoImport.state.label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>{{ videoImport.createdAt | date: 'short' }}</td>
|
<td>{{ videoImport.createdAt | date: 'short' }}</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
|
|
|
@ -7,4 +7,8 @@ pre {
|
||||||
|
|
||||||
.video-import-error {
|
.video-import-error {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
@include table-badge;
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,19 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit
|
||||||
return 'MyAccountVideoImportsComponent'
|
return 'MyAccountVideoImportsComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVideoImportStateClass (state: VideoImportState) {
|
||||||
|
switch (state) {
|
||||||
|
case VideoImportState.FAILED:
|
||||||
|
return 'badge-red'
|
||||||
|
case VideoImportState.REJECTED:
|
||||||
|
return 'badge-banned'
|
||||||
|
case VideoImportState.PENDING:
|
||||||
|
return 'badge-yellow'
|
||||||
|
default:
|
||||||
|
return 'badge-green'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isVideoImportSuccess (videoImport: VideoImport) {
|
isVideoImportSuccess (videoImport: VideoImport) {
|
||||||
return videoImport.state.id === VideoImportState.SUCCESS
|
return videoImport.state.id === VideoImportState.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
<h1>
|
<h1 class="d-flex justify-content-between">
|
||||||
<my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
|
<my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
|
||||||
</h1>
|
<ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="has-feedback has-clear">
|
||||||
<div class="video-playlists-header">
|
<input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
||||||
<input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="create-button" routerLink="create">
|
<a class="create-button" routerLink="create">
|
||||||
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
|
||||||
<ng-container i18n>Create playlist</ng-container>
|
<ng-container i18n>Create playlist</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</h1>
|
||||||
|
|
||||||
<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||||
<div *ngFor="let playlist of videoPlaylists" class="video-playlist">
|
<div *ngFor="let playlist of videoPlaylists" class="video-playlist">
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
@include create-button;
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
@include peertube-input-text(300px);
|
||||||
|
}
|
||||||
|
|
||||||
::ng-deep .action-button {
|
::ng-deep .action-button {
|
||||||
&.action-button-delete {
|
&.action-button-delete {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -33,16 +37,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-playlists-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 20px 0 50px;
|
|
||||||
|
|
||||||
input[type=text] {
|
|
||||||
@include peertube-input-text(300px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $small-view) {
|
@media screen and (max-width: $small-view) {
|
||||||
.video-playlists-header {
|
.video-playlists-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -84,6 +84,11 @@ export class MyAccountVideoPlaylistsComponent implements OnInit {
|
||||||
this.loadVideoPlaylists()
|
this.loadVideoPlaylists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetSearch () {
|
||||||
|
this.videoPlaylistsSearch = ''
|
||||||
|
this.onVideoPlaylistSearchChanged()
|
||||||
|
}
|
||||||
|
|
||||||
onVideoPlaylistSearchChanged () {
|
onVideoPlaylistSearchChanged () {
|
||||||
this.videoPlaylistSearchChanged.next()
|
this.videoPlaylistSearchChanged.next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
<h1>
|
<h1 class="d-flex justify-content-between">
|
||||||
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
|
<span>
|
||||||
<ng-container i18n>My videos</ng-container><span class="badge badge-secondary"> {{ pagination.totalItems }}</span>
|
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
|
||||||
</h1>
|
<ng-container i18n>My videos</ng-container>
|
||||||
|
<span class="badge badge-secondary"> {{ pagination.totalItems }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="videos-header">
|
<div class="has-feedback has-clear">
|
||||||
<input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" />
|
<input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" />
|
||||||
</div>
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
<my-videos-selection
|
<my-videos-selection
|
||||||
[pagination]="pagination"
|
[pagination]="pagination"
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
.videos-header {
|
input[type=text] {
|
||||||
display: flex;
|
@include peertube-input-text(300px);
|
||||||
justify-content: space-between;
|
|
||||||
margin: 20px 0 50px;
|
|
||||||
|
|
||||||
input[type=text] {
|
|
||||||
@include peertube-input-text(300px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button-delete-selection {
|
.action-button-delete-selection {
|
||||||
|
|
|
@ -59,13 +59,17 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.videosSearchChanged
|
this.videosSearchChanged
|
||||||
.pipe(
|
.pipe(debounceTime(500))
|
||||||
debounceTime(500))
|
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.videosSelection.reloadVideos()
|
this.videosSelection.reloadVideos()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetSearch () {
|
||||||
|
this.videosSearch = ''
|
||||||
|
this.onVideosSearchChanged()
|
||||||
|
}
|
||||||
|
|
||||||
onVideosSearchChanged () {
|
onVideosSearchChanged () {
|
||||||
this.videosSearchChanged.next()
|
this.videosSearchChanged.next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -381,14 +381,14 @@ export class UserService {
|
||||||
|
|
||||||
const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0)
|
const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0)
|
||||||
|
|
||||||
let videoQuotaDaily
|
let videoQuotaDaily: string
|
||||||
let videoQuotaUsedDaily
|
let videoQuotaUsedDaily: string
|
||||||
if (user.videoQuotaDaily === -1) {
|
if (user.videoQuotaDaily === -1) {
|
||||||
videoQuotaDaily = '∞'
|
videoQuotaDaily = '∞'
|
||||||
videoQuotaUsedDaily = this.bytesPipe.transform(0, 0)
|
videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + ''
|
||||||
} else {
|
} else {
|
||||||
videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0)
|
videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) + ''
|
||||||
videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0)
|
videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleLabels: { [ id in UserRole ]: string } = {
|
const roleLabels: { [ id in UserRole ]: string } = {
|
||||||
|
|
|
@ -105,13 +105,18 @@ export class UserSubscriptionService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
listSubscriptions (componentPagination: ComponentPaginationLight): Observable<ResultList<VideoChannel>> {
|
listSubscriptions (parameters: {
|
||||||
|
pagination: ComponentPaginationLight
|
||||||
|
search: string
|
||||||
|
}): Observable<ResultList<VideoChannel>> {
|
||||||
|
const { pagination, search } = parameters
|
||||||
const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
|
const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
|
||||||
|
|
||||||
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
const restPagination = this.restService.componentPaginationToRestPagination(pagination)
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination)
|
params = this.restService.addRestGetParams(params, restPagination)
|
||||||
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params })
|
return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -310,6 +310,7 @@ ngb-tooltip-window {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5rem;
|
right: .5rem;
|
||||||
height: 95%;
|
height: 95%;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(0, 0, 0, 0.7);
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
|
|
@ -690,12 +690,11 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
isolation: isolate;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: rgb(92, 92, 92);
|
color: $grey-foreground-color;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
|
|
||||||
&:nth-of-type(1) {
|
&:nth-of-type(1) {
|
||||||
|
|
|
@ -120,7 +120,8 @@ async function listAccountChannels (req: express.Request, res: express.Response)
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
withStats: req.query.withStats
|
withStats: req.query.withStats,
|
||||||
|
search: req.query.search
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultList = await VideoChannelModel.listByAccount(options)
|
const resultList = await VideoChannelModel.listByAccount(options)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
userSubscriptionAddValidator,
|
userSubscriptionAddValidator,
|
||||||
userSubscriptionGetValidator
|
userSubscriptionGetValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators'
|
import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator, userSubscriptionListValidator } from '../../../middlewares/validators'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
|
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
|
||||||
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
||||||
|
@ -45,6 +45,7 @@ mySubscriptionsRouter.get('/me/subscriptions',
|
||||||
userSubscriptionsSortValidator,
|
userSubscriptionsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
|
userSubscriptionListValidator,
|
||||||
asyncMiddleware(getUserSubscriptions)
|
asyncMiddleware(getUserSubscriptions)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -141,7 +142,13 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
const actorId = user.Account.Actor.id
|
const actorId = user.Account.Actor.id
|
||||||
|
|
||||||
const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort)
|
const resultList = await ActorFollowModel.listSubscriptionsForApi({
|
||||||
|
actorId,
|
||||||
|
start: req.query.start,
|
||||||
|
count: req.query.count,
|
||||||
|
sort: req.query.sort,
|
||||||
|
search: req.query.search
|
||||||
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,8 +119,7 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
|
||||||
actorId: serverActor.id,
|
actorId: serverActor.id,
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort
|
||||||
search: req.query.search
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -7,6 +7,18 @@ import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-v
|
||||||
import { toArray } from '../../helpers/custom-validators/misc'
|
import { toArray } from '../../helpers/custom-validators/misc'
|
||||||
import { WEBSERVER } from '../../initializers/constants'
|
import { WEBSERVER } from '../../initializers/constants'
|
||||||
|
|
||||||
|
const userSubscriptionListValidator = [
|
||||||
|
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking userSubscriptionListValidator parameters', { parameters: req.query })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const userSubscriptionAddValidator = [
|
const userSubscriptionAddValidator = [
|
||||||
body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
|
body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
|
||||||
|
|
||||||
|
@ -64,6 +76,7 @@ const userSubscriptionGetValidator = [
|
||||||
|
|
||||||
export {
|
export {
|
||||||
areSubscriptionsExistValidator,
|
areSubscriptionsExistValidator,
|
||||||
|
userSubscriptionListValidator,
|
||||||
userSubscriptionAddValidator,
|
userSubscriptionAddValidator,
|
||||||
userSubscriptionGetValidator
|
userSubscriptionGetValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,15 @@ import {
|
||||||
Max,
|
Max,
|
||||||
Model,
|
Model,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt,
|
||||||
|
Sequelize
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { FollowState } from '../../../shared/models/actors'
|
import { FollowState } from '../../../shared/models/actors'
|
||||||
import { ActorFollow } from '../../../shared/models/actors/follow.model'
|
import { ActorFollow } from '../../../shared/models/actors/follow.model'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
|
import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
import { createSafeIn, getFollowsSort, getSort } from '../utils'
|
import { createSafeIn, getFollowsSort, getSort, searchAttribute } from '../utils'
|
||||||
import { ActorModel, unusedActorAttributesForAPI } from './actor'
|
import { ActorModel, unusedActorAttributesForAPI } from './actor'
|
||||||
import { VideoChannelModel } from '../video/video-channel'
|
import { VideoChannelModel } from '../video/video-channel'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
|
@ -440,16 +441,34 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
|
static listSubscriptionsForApi (options: {
|
||||||
|
actorId: number
|
||||||
|
start: number
|
||||||
|
count: number
|
||||||
|
sort: string
|
||||||
|
search?: string
|
||||||
|
}) {
|
||||||
|
const { actorId, start, count, sort } = options
|
||||||
|
const where = {
|
||||||
|
actorId: actorId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.search) {
|
||||||
|
Object.assign(where, {
|
||||||
|
[Op.or]: [
|
||||||
|
searchAttribute(options.search, '$ActorFollowing.preferredUsername$'),
|
||||||
|
searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$')
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
attributes: [],
|
attributes: [],
|
||||||
distinct: true,
|
distinct: true,
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
where: {
|
where,
|
||||||
actorId: actorId
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
attributes: [ 'id' ],
|
attributes: [ 'id' ],
|
||||||
|
|
|
@ -315,9 +315,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
start: number
|
start: number
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
search?: string
|
|
||||||
}) {
|
}) {
|
||||||
const { actorId, search } = parameters
|
const { actorId } = parameters
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
offset: parameters.start,
|
offset: parameters.start,
|
||||||
|
@ -326,7 +325,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopes = {
|
const scopes = {
|
||||||
method: [ ScopeNames.FOR_API, { actorId, search } as AvailableForListOptions ]
|
method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
|
||||||
}
|
}
|
||||||
return VideoChannelModel
|
return VideoChannelModel
|
||||||
.scope(scopes)
|
.scope(scopes)
|
||||||
|
@ -405,7 +404,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
withStats?: boolean
|
withStats?: boolean
|
||||||
|
search?: string
|
||||||
}) {
|
}) {
|
||||||
|
const escapedSearch = VideoModel.sequelize.escape(options.search)
|
||||||
|
const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
|
||||||
|
const where = options.search
|
||||||
|
? {
|
||||||
|
[Op.or]: [
|
||||||
|
Sequelize.literal(
|
||||||
|
'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
|
||||||
|
),
|
||||||
|
Sequelize.literal(
|
||||||
|
'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
offset: options.start,
|
offset: options.start,
|
||||||
limit: options.count,
|
limit: options.count,
|
||||||
|
@ -418,7 +433,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
},
|
},
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
where
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
|
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
|
||||||
|
|
Loading…
Reference in New Issue