Fix rowsPerPage change, add filter clear button, update video-abuse-list search query...
authorRigel Kent <sendmemail@rigelk.eu>
Sun, 3 May 2020 21:01:57 +0000 (23:01 +0200)
committerRigel Kent <par@rigelk.eu>
Mon, 4 May 2020 13:01:44 +0000 (15:01 +0200)
22 files changed:
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
client/src/app/+admin/system/jobs/jobs.component.html
client/src/app/+admin/users/user-edit/user-password.component.scss
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
client/src/app/+signup/+register/register.component.scss
client/src/app/shared/rest/rest-table.ts
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/primeng-custom.scss
server/models/utils.ts
shared/models/videos/abuse/video-abuse.model.ts

index a3be5961b75aeb4d0603c4e3b00137b834bb6029..7b75bd453a4eb4b371302d7ee2f88a7115b6dfc4 100644 (file)
@@ -6,10 +6,14 @@
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <input
-        type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-        (keyup)="onSearch($event)"
-      >
+      <div class="ml-auto has-feedback has-clear">
+        <input
+          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
+          (keyup)="onSearch($event)"
+        >
+        <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>
   </ng-template>
 
index 4c232e29d77c196cd993d9058002f9b96f9054f9..5769c7b535e2886ad5a3456ec64be8d0d34831a5 100644 (file)
@@ -6,11 +6,13 @@
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto">
+      <div class="ml-auto has-feedback has-clear">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
+        <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="ml-2 follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
         <my-global-icon iconName="add"></my-global-icon>
index 99d8719a38873cfc7dd5cdd3675d14d2c48a8d5a..592287ea0e0dd623f5ed28c3751ab366656edfa4 100644 (file)
@@ -14,7 +14,7 @@
 <p-table
   [value]="videoRedundancies" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
-  (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="header">
     <tr>
index 99b4e267c0a377c1ed5a50a0027dfc2314664c84..2627056035a3e220306f17f2308be9db44a4d96e 100644 (file)
@@ -6,11 +6,13 @@
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto">
+      <div class="ml-auto has-feedback has-clear">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
+        <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>
   </ng-template>
index aecdca38738bed234b6362c7f641bb1d9877ce83..17364ae040674a710e34991cd252a6dae45ae064 100644 (file)
@@ -6,11 +6,13 @@
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto">
+      <div class="ml-auto has-feedback has-clear">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
+        <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="ml-2 block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
         <my-global-icon iconName="add"></my-global-icon>
index 704d43ac41dad0b9397381166d37f5cb60631c8b..588d383957896c43b5e66ad4803a97d60cdf7aec 100644 (file)
@@ -14,7 +14,7 @@
             alt="Avatar"
           >
           <div>
-            <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
+            <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
           </div>
         </a>
         <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 @@
             alt="Avatar"
           >
           <div>
-            <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
+            <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
           </div>
         </a>
         <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>
index 5481915b94650ccd46aae93498d47eb1c58162d1..d9cb19845f03a5440c0e466d9a872fdea7b734b6 100644 (file)
@@ -1,6 +1,7 @@
-import { Component, ViewEncapsulation, Input } from '@angular/core'
-import { VideoAbuse } from '../../../../../../shared'
+import { Component, Input } from '@angular/core'
 import { Account } from '@app/shared/account/account.model'
+import { Actor } from '@app/shared/actor/actor.model'
+import { ProcessedVideoAbuse } from './video-abuse-list.component'
 
 @Component({
   selector: 'my-video-abuse-details',
@@ -8,9 +9,9 @@ import { Account } from '@app/shared/account/account.model'
   styleUrls: [ '../moderation.component.scss' ]
 })
 export class VideoAbuseDetailsComponent {
-  @Input() videoAbuse: VideoAbuse
+  @Input() videoAbuse: ProcessedVideoAbuse
 
-  createByString (account: Account) {
-    return Account.CREATE_BY_STRING(account.name, account.host)
+  switchToDefaultAvatar ($event: Event) {
+    ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
   }
 }
index 2e7b60e2f708ff93636ad251c2a757ea9bd2a886..ba05073cf1b6d96a22ad407d26e18c4ffe9660fb 100644 (file)
@@ -3,25 +3,19 @@
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
-  (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
       <div class="ml-auto">
-        <div class="input-group">
+        <div class="input-group has-feedback has-clear">
           <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
             <div class="input-group-text" ngbDropdownToggle>
               <span class="caret" aria-haspopup="menu" role="button"></span>
             </div>
 
             <div role="menu" ngbDropdownMenu>
-              <h6 class="dropdown-header" i18n>Filter reports</h6>
-
-              <!-- TODO:
-              <div class="dropdown-item" i18n>Reports opened by admins</div>
-              <div class="dropdown-item" i18n>Reports on videos with multiple reports</div>
-              <div class="dropdown-item" i18n>Unassigned reports</div>
-              -->
+              <h6 class="dropdown-header" i18n>Advanced report filters</h6>
               <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
               <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
               <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
           </div>
           <input
             type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-            (keyup)="onSearch($event)"
+            (keyup)="onAbuseSearch($event)"
           >
+          <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
+          <span class="sr-only" i18n>Clear filters</span>
         </div>
       </div>
     </div>
@@ -68,7 +64,7 @@
             >
             <div>
               {{ videoAbuse.reporterAccount.displayName }}
-              <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
+              <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
             </div>
           </div>
         </a>
index 83d194d5246ede884157ebd906061c73e8630f2d..f54e3dccd4578cfb5a9f12b4c5b06228503c55ef 100644 (file)
@@ -16,9 +16,23 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
 import { DomSanitizer } from '@angular/platform-browser'
 import { BlocklistService } from '@app/shared/blocklist'
 import { VideoService } from '@app/shared/video/video.service'
-import { ActivatedRoute } from '@angular/router'
+import { ActivatedRoute, Params, Router } from '@angular/router'
 import { filter } from 'rxjs/operators'
 
+export type ProcessedVideoAbuse = VideoAbuse & {
+  moderationCommentHtml?: string,
+  reasonHtml?: string
+  embedHtml?: string
+  updatedAt?: Date
+  // override bare server-side definitions with rich client-side definitions
+  reporterAccount: Account
+  video: VideoAbuse['video'] & {
+    channel: VideoAbuse['video']['channel'] & {
+      ownerAccount: Account
+    }
+  }
+}
+
 @Component({
   selector: 'my-video-abuse-list',
   templateUrl: './video-abuse-list.component.html',
@@ -27,7 +41,7 @@ import { filter } from 'rxjs/operators'
 export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit {
   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
 
-  videoAbuses: (VideoAbuse & { moderationCommentHtml?: string, reasonHtml?: string })[] = []
+  videoAbuses: ProcessedVideoAbuse[] = []
   totalRecords = 0
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
@@ -44,7 +58,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
     private i18n: I18n,
     private markdownRenderer: MarkdownService,
     private sanitizer: DomSanitizer,
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private router: Router
   ) {
     super()
 
@@ -212,15 +227,24 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
     this.loadData()
   }
 
-  createByString (account: Account) {
-    return Account.CREATE_BY_STRING(account.name, account.host)
+  /* Table filter functions */
+  onAbuseSearch (event: Event) {
+    this.onSearch(event)
+    this.setQueryParams((event.target as HTMLInputElement).value)
+  }
+
+  setQueryParams (search: string) {
+    const queryParams: Params = {}
+    if (search) Object.assign(queryParams, { search })
+    this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
   }
 
-  setTableFilter (filter: string) {
-    // FIXME: cannot use ViewChild, so create a component for the filter input
-    const filterInput = document.getElementById('table-filter') as HTMLInputElement
-    if (filterInput) filterInput.value = filter
+  resetTableFilter () {
+    this.setTableFilter('')
+    this.setQueryParams('')
+    this.resetSearch()
   }
+  /* END Table filter functions */
 
   isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
     return videoAbuse.state.id === VideoAbuseState.ACCEPTED
@@ -279,17 +303,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
     }).subscribe(
         async resultList => {
           this.totalRecords = resultList.total
+          this.videoAbuses = []
 
-          this.videoAbuses = resultList.data
-
-          for (const abuse of this.videoAbuses) {
+          for (const abuse of resultList.data) {
             Object.assign(abuse, {
               reasonHtml: await this.toHtml(abuse.reason),
               moderationCommentHtml: await this.toHtml(abuse.moderationComment),
               embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
               reporterAccount: new Account(abuse.reporterAccount)
             })
+
+            if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
             if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
+
+            this.videoAbuses.push(abuse as ProcessedVideoAbuse)
           }
 
         },
index b3f7789df6b6607dfa0d11b81ce27aca7de867b8..eb194b023129e18cbfbaf93ceaef0dd21a486fca 100644 (file)
@@ -3,15 +3,17 @@
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} blacklisted videos"
-  (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div class="ml-auto">
+      <div class="ml-auto has-feedback has-clear">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
+        <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>
   </ng-template>
index 05d5731633cee2997d4bc64186bb9f8dc4ccf81b..038dfa522215d0fcfb6d26ec539b548cc93d37ba 100644 (file)
@@ -21,7 +21,7 @@
 <p-table
   [value]="jobs" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" dataKey="uniqId"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [first]="pagination.start"
-  [tableStyle]="{'table-layout':'auto'}" (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  [tableStyle]="{'table-layout':'auto'}" (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="header">
     <tr>
index 5cd93f6af140fe2b43e37aca49759343507ece40..217d585afcc144a00894f16a9e7e72a9b55833f0 100644 (file)
@@ -16,3 +16,7 @@ input[type=submit] {
 
   margin-top: 10px;
 }
+
+.input-group-append {
+  height: 30px;
+}
index 94c59cb9a89cc6c86465e5927633b3cc8f2ac2d1..8b71dae79d4fe9fbbdb5214cfba7caabeed3f507 100644 (file)
@@ -8,12 +8,12 @@
 </div>
 
 <p-table
-  [value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+  [value]="users" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
   [(selection)]="selectedUsers"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
-  (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
         </my-action-dropdown>
       </div>
 
-      <div>
+      <div class="has-feedback has-clear">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
+        <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>
   </ng-template>
index ba27ee7ff1e33c56cabffe8e0f207113fb1576de..8f8af655cc7804fb6378739a9c1ec2acd936c933 100644 (file)
@@ -19,6 +19,10 @@ my-actor-avatar-info {
   @include peertube-input-group(fit-content);
 }
 
+.input-group-append {
+  height: 30px;
+}
+
 input {
   &[type=text] {
     @include peertube-input-text(340px);
index 7d447cdb363c92f1f9fe89d78ec1ff9164090a76..37c6ad6b4b056e11631bc5b2e09a1b88b4fde2f5 100644 (file)
@@ -1,7 +1,7 @@
 <p-table
   [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
-  (onPage)="onPage()" [expandedRowKeys]="expandedRows"
+  (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
 >
   <ng-template pTemplate="header">
     <tr>
index cc60ef524785571dce799812feb7b88bd89dd12a..e135b5cb4312488b91a7f4bb3845051f01d5c9bb 100644 (file)
   @include peertube-input-group(400px);
 }
 
+.input-group-append {
+  height: 30px;
+}
+
 input:not([type=submit]) {
   @include peertube-input-text(400px);
 
index 4dd0f5ff30d4eb71816baf2678ce4e5c97d7db4e..d4e6cf5f2c877b78701895429f2f06098860f3d1 100644 (file)
@@ -74,10 +74,29 @@ export abstract class RestTable {
     this.searchStream.next(target.value)
   }
 
-  onPage () {
+  onPage (event: { first: number, rows: number }) {
+    if (this.rowsPerPage !== event.rows) {
+      this.rowsPerPage = event.rows
+      this.pagination = {
+        start: event.first,
+        count: this.rowsPerPage
+      }
+      this.loadData()
+    }
     this.expandedRows = {}
   }
 
+  setTableFilter (filter: string) {
+    // FIXME: cannot use ViewChild, so create a component for the filter input
+    const filterInput = document.getElementById('table-filter') as HTMLInputElement
+    if (filterInput) filterInput.value = filter
+  }
+
+  resetSearch () {
+    this.searchStream.next('')
+    this.setTableFilter('')
+  }
+
   protected abstract loadData (): void
 
   private getSortLocalStorageKey () {
index 62503fc0254f4de7303b0680448d393cf6e7a32a..bbecd8ba803e94fc2cd35c232182b4532bf1b3dd 100644 (file)
@@ -339,6 +339,11 @@ table {
       .peertube-select-container {
         width: 100% !important;
       }
+
+      .caption input[type=text] {
+        width: unset !important;
+        flex-grow: 1;
+      }
     }
   }
 }
index 50f1dafedd7497f2a21df2685a1b97fc458ba54e..cb266cc68a3f80ad00ffd3a70967f12863b43c5c 100644 (file)
@@ -27,7 +27,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 /* rules for dropdowns excepts when in button group, to avoid impacting the dropdown-toggle */
-.dropdown:not(.btn-group):not(.dropdown-root):not(.action-dropdown) {
+.dropdown:not(.btn-group):not(.dropdown-root):not(.action-dropdown):not(.input-group-prepend) {
   z-index: z(dropdown) !important;
 
   &.list-overflow-menu,
@@ -270,10 +270,9 @@ ngb-tooltip-window {
   & > .form-control {
     flex: initial;
   }
-
-  .input-group-prepend,
-  .input-group-append {
-    height: 30px;
+  input.form-control {
+    width: unset !important;
+    flex-grow: 1;
   }
 
   .input-group-prepend + input {
@@ -281,3 +280,35 @@ ngb-tooltip-window {
     border-bottom-left-radius: 0 !important;
   }
 }
+
+.has-feedback.has-clear {
+  position: relative;
+
+  input {
+    padding-right: 1.5rem !important;
+  }
+
+  .form-control-clear {
+    color: rgba(0, 0, 0, 0.4);
+    /*
+     * Enable pointer events as they have been disabled since Bootstrap 3.3
+     * See https://github.com/twbs/bootstrap/pull/14104
+     */
+    pointer-events: all;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    right: .5rem;
+    height: 95%;
+
+    &:hover {
+      color: rgba(0, 0, 0, 0.7);
+      cursor: pointer;
+    }
+  }
+
+  input:placeholder-shown + .form-control-clear {
+    display: none;
+  }
+}
index eab2b2dfd3e3c733f3208ee938d53d068d42a7a3..d48f2dfc45d5db64b20617f1559bd15f911f7845 100644 (file)
@@ -30,7 +30,8 @@ p-table {
 
     .caption {
       height: 40px;
-      display: flex;
+      width: 100%;
+      display: inline-flex;
       align-items: center;
 
       .input-group-text {
index 3e3825b3267fd7ecc63947bd2a6fcb666f10ef15..956562e705393f9ff4d3e8a3e3d612d1568fd18f 100644 (file)
@@ -223,9 +223,12 @@ interface QueryStringFilterPrefixes {
   [key: string]: string | { prefix: string, handler: Function, multiple?: boolean }
 }
 
-function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes) {
+function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): {
+  search: string
+  [key: string]: string | number | string[] | number[]
+} {
   const tokens = q // tokenize only if we have a querystring
-    ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean)
+    ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean) // split by space unless using double quotes
     : []
 
   // TODO: when Typescript supports Object.fromEntries, replace with the Object method
@@ -252,16 +255,18 @@ function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes)
       }
     })).join(' '),
     // filters defined in prefixes are added under their own name
-    ...objectMap(prefixes, v => {
-      if (typeof v === "string") {
-        return tokens.filter(e => e.startsWith(v)).map(e => e.slice(v.length))
+    ...objectMap(prefixes, p => {
+      if (typeof p === "string") {
+        return tokens.filter(e => e.startsWith(p)).map(e => e.slice(p.length)) // we keep the matched item, and remove its prefix
       } else {
-        const _tokens = tokens.filter(e => e.startsWith(v.prefix)).map(e => e.slice(v.prefix.length)).map(v.handler)
-        return !v.multiple
-          ? _tokens.length > 0
-            ? _tokens[0]
-            : ''
-          : _tokens
+        const _tokens = tokens.filter(e => e.startsWith(p.prefix)).map(e => e.slice(p.prefix.length)).map(p.handler)
+        // multiple is false by default, meaning we usually just keep the first occurence of a given prefix
+        if (!p.multiple && _tokens.length > 0) {
+          return _tokens[0]
+        } else if (!p.multiple) {
+          return ''
+        }
+        return _tokens
       }
     })
   }
index bbef7f4f9db6b9f251229dbe344b65843f44f98b..f2c2cdc415d99b07d2b9079f0d86fab7ee721474 100644 (file)
@@ -23,7 +23,7 @@ export interface VideoAbuse {
   }
 
   createdAt: Date
-  updatedAt?: Date
+  updatedAt: Date
 
   count?: number
   nth?: number