aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-05-03 23:01:57 +0200
committerRigel Kent <par@rigelk.eu>2020-05-04 15:01:44 +0200
commit25a42e293be90d35afad2096e9db2fa3d617d855 (patch)
tree5739da8e2aed75447cf0fd608089cc8849c0da33
parent801d957155d574bda984206021cdd1fe58ef56b9 (diff)
downloadPeerTube-25a42e293be90d35afad2096e9db2fa3d617d855.tar.gz
PeerTube-25a42e293be90d35afad2096e9db2fa3d617d855.tar.zst
PeerTube-25a42e293be90d35afad2096e9db2fa3d617d855.zip
Fix rowsPerPage change, add filter clear button, update video-abuse-list search query param dynamically
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html12
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html4
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html4
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts11
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html18
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts51
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html6
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss4
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html8
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html2
-rw-r--r--client/src/app/+signup/+register/register.component.scss4
-rw-r--r--client/src/app/shared/rest/rest-table.ts21
-rw-r--r--client/src/sass/application.scss5
-rw-r--r--client/src/sass/bootstrap.scss41
-rw-r--r--client/src/sass/primeng-custom.scss3
-rw-r--r--server/models/utils.ts27
-rw-r--r--shared/models/videos/abuse/video-abuse.model.ts2
22 files changed, 175 insertions, 64 deletions
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index a3be5961b..7b75bd453 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -6,10 +6,14 @@
6> 6>
7 <ng-template pTemplate="caption"> 7 <ng-template pTemplate="caption">
8 <div class="caption"> 8 <div class="caption">
9 <input 9 <div class="ml-auto has-feedback has-clear">
10 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 10 <input
11 (keyup)="onSearch($event)" 11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
12 > 12 (keyup)="onSearch($event)"
13 >
14 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
15 <span class="sr-only" i18n>Clear filters</span>
16 </div>
13 </div> 17 </div>
14 </ng-template> 18 </ng-template>
15 19
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 4c232e29d..5769c7b53 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -6,11 +6,13 @@
6> 6>
7 <ng-template pTemplate="caption"> 7 <ng-template pTemplate="caption">
8 <div class="caption"> 8 <div class="caption">
9 <div class="ml-auto"> 9 <div class="ml-auto has-feedback has-clear">
10 <input 10 <input
11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
12 (keyup)="onSearch($event)" 12 (keyup)="onSearch($event)"
13 > 13 >
14 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
15 <span class="sr-only" i18n>Clear filters</span>
14 </div> 16 </div>
15 <a class="ml-2 follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()"> 17 <a class="ml-2 follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
16 <my-global-icon iconName="add"></my-global-icon> 18 <my-global-icon iconName="add"></my-global-icon>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
index 99d8719a3..592287ea0 100644
--- a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
@@ -14,7 +14,7 @@
14<p-table 14<p-table
15 [value]="videoRedundancies" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" 15 [value]="videoRedundancies" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
16 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 16 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
17 (onPage)="onPage()" [expandedRowKeys]="expandedRows" 17 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
18> 18>
19 <ng-template pTemplate="header"> 19 <ng-template pTemplate="header">
20 <tr> 20 <tr>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
index 99b4e267c..262705603 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -6,11 +6,13 @@
6> 6>
7 <ng-template pTemplate="caption"> 7 <ng-template pTemplate="caption">
8 <div class="caption"> 8 <div class="caption">
9 <div class="ml-auto"> 9 <div class="ml-auto has-feedback has-clear">
10 <input 10 <input
11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
12 (keyup)="onSearch($event)" 12 (keyup)="onSearch($event)"
13 > 13 >
14 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
15 <span class="sr-only" i18n>Clear filters</span>
14 </div> 16 </div>
15 </div> 17 </div>
16 </ng-template> 18 </ng-template>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
index aecdca387..17364ae04 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -6,11 +6,13 @@
6> 6>
7 <ng-template pTemplate="caption"> 7 <ng-template pTemplate="caption">
8 <div class="caption"> 8 <div class="caption">
9 <div class="ml-auto"> 9 <div class="ml-auto has-feedback has-clear">
10 <input 10 <input
11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
12 (keyup)="onSearch($event)" 12 (keyup)="onSearch($event)"
13 > 13 >
14 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
15 <span class="sr-only" i18n>Clear filters</span>
14 </div> 16 </div>
15 <a class="ml-2 block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()"> 17 <a class="ml-2 block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
16 <my-global-icon iconName="add"></my-global-icon> 18 <my-global-icon iconName="add"></my-global-icon>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
index 704d43ac4..588d38395 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
@@ -14,7 +14,7 @@
14 alt="Avatar" 14 alt="Avatar"
15 > 15 >
16 <div> 16 <div>
17 <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span> 17 <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
18 </div> 18 </div>
19 </a> 19 </a>
20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n> 20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
@@ -34,7 +34,7 @@
34 alt="Avatar" 34 alt="Avatar"
35 > 35 >
36 <div> 36 <div>
37 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span> 37 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
38 </div> 38 </div>
39 </a> 39 </a>
40 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n> 40 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
index 5481915b9..d9cb19845 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
@@ -1,6 +1,7 @@
1import { Component, ViewEncapsulation, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { VideoAbuse } from '../../../../../../shared'
3import { Account } from '@app/shared/account/account.model' 2import { Account } from '@app/shared/account/account.model'
3import { Actor } from '@app/shared/actor/actor.model'
4import { ProcessedVideoAbuse } from './video-abuse-list.component'
4 5
5@Component({ 6@Component({
6 selector: 'my-video-abuse-details', 7 selector: 'my-video-abuse-details',
@@ -8,9 +9,9 @@ import { Account } from '@app/shared/account/account.model'
8 styleUrls: [ '../moderation.component.scss' ] 9 styleUrls: [ '../moderation.component.scss' ]
9}) 10})
10export class VideoAbuseDetailsComponent { 11export class VideoAbuseDetailsComponent {
11 @Input() videoAbuse: VideoAbuse 12 @Input() videoAbuse: ProcessedVideoAbuse
12 13
13 createByString (account: Account) { 14 switchToDefaultAvatar ($event: Event) {
14 return Account.CREATE_BY_STRING(account.name, account.host) 15 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
15 } 16 }
16} 17}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index 2e7b60e2f..ba05073cf 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -3,25 +3,19 @@
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" 5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
6 (onPage)="onPage()" [expandedRowKeys]="expandedRows" 6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
7> 7>
8 <ng-template pTemplate="caption"> 8 <ng-template pTemplate="caption">
9 <div class="caption"> 9 <div class="caption">
10 <div class="ml-auto"> 10 <div class="ml-auto">
11 <div class="input-group"> 11 <div class="input-group has-feedback has-clear">
12 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> 12 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
13 <div class="input-group-text" ngbDropdownToggle> 13 <div class="input-group-text" ngbDropdownToggle>
14 <span class="caret" aria-haspopup="menu" role="button"></span> 14 <span class="caret" aria-haspopup="menu" role="button"></span>
15 </div> 15 </div>
16 16
17 <div role="menu" ngbDropdownMenu> 17 <div role="menu" ngbDropdownMenu>
18 <h6 class="dropdown-header" i18n>Filter reports</h6> 18 <h6 class="dropdown-header" i18n>Advanced report filters</h6>
19
20 <!-- TODO:
21 <div class="dropdown-item" i18n>Reports opened by admins</div>
22 <div class="dropdown-item" i18n>Reports on videos with multiple reports</div>
23 <div class="dropdown-item" i18n>Unassigned reports</div>
24 -->
25 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> 19 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
26 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> 20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
27 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> 21 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
@@ -31,8 +25,10 @@
31 </div> 25 </div>
32 <input 26 <input
33 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
34 (keyup)="onSearch($event)" 28 (keyup)="onAbuseSearch($event)"
35 > 29 >
30 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
31 <span class="sr-only" i18n>Clear filters</span>
36 </div> 32 </div>
37 </div> 33 </div>
38 </div> 34 </div>
@@ -68,7 +64,7 @@
68 > 64 >
69 <div> 65 <div>
70 {{ videoAbuse.reporterAccount.displayName }} 66 {{ videoAbuse.reporterAccount.displayName }}
71 <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span> 67 <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
72 </div> 68 </div>
73 </div> 69 </div>
74 </a> 70 </a>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
index 83d194d52..f54e3dccd 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
@@ -16,9 +16,23 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
16import { DomSanitizer } from '@angular/platform-browser' 16import { DomSanitizer } from '@angular/platform-browser'
17import { BlocklistService } from '@app/shared/blocklist' 17import { BlocklistService } from '@app/shared/blocklist'
18import { VideoService } from '@app/shared/video/video.service' 18import { VideoService } from '@app/shared/video/video.service'
19import { ActivatedRoute } from '@angular/router' 19import { ActivatedRoute, Params, Router } from '@angular/router'
20import { filter } from 'rxjs/operators' 20import { filter } from 'rxjs/operators'
21 21
22export type ProcessedVideoAbuse = VideoAbuse & {
23 moderationCommentHtml?: string,
24 reasonHtml?: string
25 embedHtml?: string
26 updatedAt?: Date
27 // override bare server-side definitions with rich client-side definitions
28 reporterAccount: Account
29 video: VideoAbuse['video'] & {
30 channel: VideoAbuse['video']['channel'] & {
31 ownerAccount: Account
32 }
33 }
34}
35
22@Component({ 36@Component({
23 selector: 'my-video-abuse-list', 37 selector: 'my-video-abuse-list',
24 templateUrl: './video-abuse-list.component.html', 38 templateUrl: './video-abuse-list.component.html',
@@ -27,7 +41,7 @@ import { filter } from 'rxjs/operators'
27export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { 41export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit {
28 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent 42 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
29 43
30 videoAbuses: (VideoAbuse & { moderationCommentHtml?: string, reasonHtml?: string })[] = [] 44 videoAbuses: ProcessedVideoAbuse[] = []
31 totalRecords = 0 45 totalRecords = 0
32 sort: SortMeta = { field: 'createdAt', order: 1 } 46 sort: SortMeta = { field: 'createdAt', order: 1 }
33 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 47 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
@@ -44,7 +58,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
44 private i18n: I18n, 58 private i18n: I18n,
45 private markdownRenderer: MarkdownService, 59 private markdownRenderer: MarkdownService,
46 private sanitizer: DomSanitizer, 60 private sanitizer: DomSanitizer,
47 private route: ActivatedRoute 61 private route: ActivatedRoute,
62 private router: Router
48 ) { 63 ) {
49 super() 64 super()
50 65
@@ -212,15 +227,24 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
212 this.loadData() 227 this.loadData()
213 } 228 }
214 229
215 createByString (account: Account) { 230 /* Table filter functions */
216 return Account.CREATE_BY_STRING(account.name, account.host) 231 onAbuseSearch (event: Event) {
232 this.onSearch(event)
233 this.setQueryParams((event.target as HTMLInputElement).value)
234 }
235
236 setQueryParams (search: string) {
237 const queryParams: Params = {}
238 if (search) Object.assign(queryParams, { search })
239 this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
217 } 240 }
218 241
219 setTableFilter (filter: string) { 242 resetTableFilter () {
220 // FIXME: cannot use ViewChild, so create a component for the filter input 243 this.setTableFilter('')
221 const filterInput = document.getElementById('table-filter') as HTMLInputElement 244 this.setQueryParams('')
222 if (filterInput) filterInput.value = filter 245 this.resetSearch()
223 } 246 }
247 /* END Table filter functions */
224 248
225 isVideoAbuseAccepted (videoAbuse: VideoAbuse) { 249 isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
226 return videoAbuse.state.id === VideoAbuseState.ACCEPTED 250 return videoAbuse.state.id === VideoAbuseState.ACCEPTED
@@ -279,17 +303,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
279 }).subscribe( 303 }).subscribe(
280 async resultList => { 304 async resultList => {
281 this.totalRecords = resultList.total 305 this.totalRecords = resultList.total
306 this.videoAbuses = []
282 307
283 this.videoAbuses = resultList.data 308 for (const abuse of resultList.data) {
284
285 for (const abuse of this.videoAbuses) {
286 Object.assign(abuse, { 309 Object.assign(abuse, {
287 reasonHtml: await this.toHtml(abuse.reason), 310 reasonHtml: await this.toHtml(abuse.reason),
288 moderationCommentHtml: await this.toHtml(abuse.moderationComment), 311 moderationCommentHtml: await this.toHtml(abuse.moderationComment),
289 embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), 312 embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
290 reporterAccount: new Account(abuse.reporterAccount) 313 reporterAccount: new Account(abuse.reporterAccount)
291 }) 314 })
315
316 if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
292 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt 317 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
318
319 this.videoAbuses.push(abuse as ProcessedVideoAbuse)
293 } 320 }
294 321
295 }, 322 },
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index b3f7789df..eb194b023 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -3,15 +3,17 @@
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} blacklisted videos" 5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} blacklisted videos"
6 (onPage)="onPage()" [expandedRowKeys]="expandedRows" 6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
7> 7>
8 <ng-template pTemplate="caption"> 8 <ng-template pTemplate="caption">
9 <div class="caption"> 9 <div class="caption">
10 <div class="ml-auto"> 10 <div class="ml-auto has-feedback has-clear">
11 <input 11 <input
12 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 12 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
13 (keyup)="onSearch($event)" 13 (keyup)="onSearch($event)"
14 > 14 >
15 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
16 <span class="sr-only" i18n>Clear filters</span>
15 </div> 17 </div>
16 </div> 18 </div>
17 </ng-template> 19 </ng-template>
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html
index 05d573163..038dfa522 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.html
+++ b/client/src/app/+admin/system/jobs/jobs.component.html
@@ -21,7 +21,7 @@
21<p-table 21<p-table
22 [value]="jobs" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" dataKey="uniqId" 22 [value]="jobs" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" dataKey="uniqId"
23 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [first]="pagination.start" 23 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [first]="pagination.start"
24 [tableStyle]="{'table-layout':'auto'}" (onPage)="onPage()" [expandedRowKeys]="expandedRows" 24 [tableStyle]="{'table-layout':'auto'}" (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
25> 25>
26 <ng-template pTemplate="header"> 26 <ng-template pTemplate="header">
27 <tr> 27 <tr>
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss
index 5cd93f6af..217d585af 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -16,3 +16,7 @@ input[type=submit] {
16 16
17 margin-top: 10px; 17 margin-top: 10px;
18} 18}
19
20.input-group-append {
21 height: 30px;
22}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 94c59cb9a..8b71dae79 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -8,12 +8,12 @@
8</div> 8</div>
9 9
10<p-table 10<p-table
11 [value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" 11 [value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
13 [(selection)]="selectedUsers" 13 [(selection)]="selectedUsers"
14 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 14 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
15 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" 15 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
16 (onPage)="onPage()" [expandedRowKeys]="expandedRows" 16 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
17> 17>
18 <ng-template pTemplate="caption"> 18 <ng-template pTemplate="caption">
19 <div class="caption"> 19 <div class="caption">
@@ -25,11 +25,13 @@
25 </my-action-dropdown> 25 </my-action-dropdown>
26 </div> 26 </div>
27 27
28 <div> 28 <div class="has-feedback has-clear">
29 <input 29 <input
30 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 30 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
31 (keyup)="onSearch($event)" 31 (keyup)="onSearch($event)"
32 > 32 >
33 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
34 <span class="sr-only" i18n>Clear filters</span>
33 </div> 35 </div>
34 </div> 36 </div>
35 </ng-template> 37 </ng-template>
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
index ba27ee7ff..8f8af655c 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
@@ -19,6 +19,10 @@ my-actor-avatar-info {
19 @include peertube-input-group(fit-content); 19 @include peertube-input-group(fit-content);
20} 20}
21 21
22.input-group-append {
23 height: 30px;
24}
25
22input { 26input {
23 &[type=text] { 27 &[type=text] {
24 @include peertube-input-text(340px); 28 @include peertube-input-text(340px);
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
index 7d447cdb3..37c6ad6b4 100644
--- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
+++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
@@ -1,7 +1,7 @@
1<p-table 1<p-table
2 [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" 2 [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
4 (onPage)="onPage()" [expandedRowKeys]="expandedRows" 4 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
5> 5>
6 <ng-template pTemplate="header"> 6 <ng-template pTemplate="header">
7 <tr> 7 <tr>
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index cc60ef524..e135b5cb4 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -58,6 +58,10 @@
58 @include peertube-input-group(400px); 58 @include peertube-input-group(400px);
59} 59}
60 60
61.input-group-append {
62 height: 30px;
63}
64
61input:not([type=submit]) { 65input:not([type=submit]) {
62 @include peertube-input-text(400px); 66 @include peertube-input-text(400px);
63 67
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
index 4dd0f5ff3..d4e6cf5f2 100644
--- a/client/src/app/shared/rest/rest-table.ts
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -74,10 +74,29 @@ export abstract class RestTable {
74 this.searchStream.next(target.value) 74 this.searchStream.next(target.value)
75 } 75 }
76 76
77 onPage () { 77 onPage (event: { first: number, rows: number }) {
78 if (this.rowsPerPage !== event.rows) {
79 this.rowsPerPage = event.rows
80 this.pagination = {
81 start: event.first,
82 count: this.rowsPerPage
83 }
84 this.loadData()
85 }
78 this.expandedRows = {} 86 this.expandedRows = {}
79 } 87 }
80 88
89 setTableFilter (filter: string) {
90 // FIXME: cannot use ViewChild, so create a component for the filter input
91 const filterInput = document.getElementById('table-filter') as HTMLInputElement
92 if (filterInput) filterInput.value = filter
93 }
94
95 resetSearch () {
96 this.searchStream.next('')
97 this.setTableFilter('')
98 }
99
81 protected abstract loadData (): void 100 protected abstract loadData (): void
82 101
83 private getSortLocalStorageKey () { 102 private getSortLocalStorageKey () {
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 62503fc02..bbecd8ba8 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -339,6 +339,11 @@ table {
339 .peertube-select-container { 339 .peertube-select-container {
340 width: 100% !important; 340 width: 100% !important;
341 } 341 }
342
343 .caption input[type=text] {
344 width: unset !important;
345 flex-grow: 1;
346 }
342 } 347 }
343 } 348 }
344} 349}
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 50f1dafed..cb266cc68 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -27,7 +27,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
27} 27}
28 28
29/* rules for dropdowns excepts when in button group, to avoid impacting the dropdown-toggle */ 29/* rules for dropdowns excepts when in button group, to avoid impacting the dropdown-toggle */
30.dropdown:not(.btn-group):not(.dropdown-root):not(.action-dropdown) { 30.dropdown:not(.btn-group):not(.dropdown-root):not(.action-dropdown):not(.input-group-prepend) {
31 z-index: z(dropdown) !important; 31 z-index: z(dropdown) !important;
32 32
33 &.list-overflow-menu, 33 &.list-overflow-menu,
@@ -270,10 +270,9 @@ ngb-tooltip-window {
270 & > .form-control { 270 & > .form-control {
271 flex: initial; 271 flex: initial;
272 } 272 }
273 273 input.form-control {
274 .input-group-prepend, 274 width: unset !important;
275 .input-group-append { 275 flex-grow: 1;
276 height: 30px;
277 } 276 }
278 277
279 .input-group-prepend + input { 278 .input-group-prepend + input {
@@ -281,3 +280,35 @@ ngb-tooltip-window {
281 border-bottom-left-radius: 0 !important; 280 border-bottom-left-radius: 0 !important;
282 } 281 }
283} 282}
283
284.has-feedback.has-clear {
285 position: relative;
286
287 input {
288 padding-right: 1.5rem !important;
289 }
290
291 .form-control-clear {
292 color: rgba(0, 0, 0, 0.4);
293 /*
294 * Enable pointer events as they have been disabled since Bootstrap 3.3
295 * See https://github.com/twbs/bootstrap/pull/14104
296 */
297 pointer-events: all;
298 display: flex;
299 justify-content: center;
300 align-items: center;
301 position: absolute;
302 right: .5rem;
303 height: 95%;
304
305 &:hover {
306 color: rgba(0, 0, 0, 0.7);
307 cursor: pointer;
308 }
309 }
310
311 input:placeholder-shown + .form-control-clear {
312 display: none;
313 }
314}
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index eab2b2dfd..d48f2dfc4 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -30,7 +30,8 @@ p-table {
30 30
31 .caption { 31 .caption {
32 height: 40px; 32 height: 40px;
33 display: flex; 33 width: 100%;
34 display: inline-flex;
34 align-items: center; 35 align-items: center;
35 36
36 .input-group-text { 37 .input-group-text {
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 3e3825b32..956562e70 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -223,9 +223,12 @@ interface QueryStringFilterPrefixes {
223 [key: string]: string | { prefix: string, handler: Function, multiple?: boolean } 223 [key: string]: string | { prefix: string, handler: Function, multiple?: boolean }
224} 224}
225 225
226function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes) { 226function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): {
227 search: string
228 [key: string]: string | number | string[] | number[]
229} {
227 const tokens = q // tokenize only if we have a querystring 230 const tokens = q // tokenize only if we have a querystring
228 ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean) 231 ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean) // split by space unless using double quotes
229 : [] 232 : []
230 233
231 // TODO: when Typescript supports Object.fromEntries, replace with the Object method 234 // TODO: when Typescript supports Object.fromEntries, replace with the Object method
@@ -252,16 +255,18 @@ function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes)
252 } 255 }
253 })).join(' '), 256 })).join(' '),
254 // filters defined in prefixes are added under their own name 257 // filters defined in prefixes are added under their own name
255 ...objectMap(prefixes, v => { 258 ...objectMap(prefixes, p => {
256 if (typeof v === "string") { 259 if (typeof p === "string") {
257 return tokens.filter(e => e.startsWith(v)).map(e => e.slice(v.length)) 260 return tokens.filter(e => e.startsWith(p)).map(e => e.slice(p.length)) // we keep the matched item, and remove its prefix
258 } else { 261 } else {
259 const _tokens = tokens.filter(e => e.startsWith(v.prefix)).map(e => e.slice(v.prefix.length)).map(v.handler) 262 const _tokens = tokens.filter(e => e.startsWith(p.prefix)).map(e => e.slice(p.prefix.length)).map(p.handler)
260 return !v.multiple 263 // multiple is false by default, meaning we usually just keep the first occurence of a given prefix
261 ? _tokens.length > 0 264 if (!p.multiple && _tokens.length > 0) {
262 ? _tokens[0] 265 return _tokens[0]
263 : '' 266 } else if (!p.multiple) {
264 : _tokens 267 return ''
268 }
269 return _tokens
265 } 270 }
266 }) 271 })
267 } 272 }
diff --git a/shared/models/videos/abuse/video-abuse.model.ts b/shared/models/videos/abuse/video-abuse.model.ts
index bbef7f4f9..f2c2cdc41 100644
--- a/shared/models/videos/abuse/video-abuse.model.ts
+++ b/shared/models/videos/abuse/video-abuse.model.ts
@@ -23,7 +23,7 @@ export interface VideoAbuse {
23 } 23 }
24 24
25 createdAt: Date 25 createdAt: Date
26 updatedAt?: Date 26 updatedAt: Date
27 27
28 count?: number 28 count?: number
29 nth?: number 29 nth?: number