search filtering improvements per #1654
authorRigel Kent <sendmemail@rigelk.eu>
Wed, 4 Dec 2019 16:12:23 +0000 (17:12 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Thu, 5 Dec 2019 08:06:01 +0000 (09:06 +0100)
client/src/app/search/advanced-search.model.ts
client/src/app/search/search-filters.component.html
client/src/app/search/search-filters.component.scss
client/src/app/search/search-filters.component.ts
client/src/app/search/search.module.ts
client/src/app/videos/+video-edit/shared/video-edit.component.scss
client/src/sass/include/_miniature.scss

index e2a0253f44bb98c4a117005ffd6d9fac4254282b..50f00bc2713183449443d497abe421982751217b 100644 (file)
@@ -65,7 +65,7 @@ export class AdvancedSearch {
     for (const k of Object.keys(obj)) {
       if (k === 'sort') continue // Exception
 
-      if (obj[k] !== undefined) return true
+      if (obj[k] !== undefined && obj[k] !== '') return true
     }
 
     return false
@@ -131,7 +131,7 @@ export class AdvancedSearch {
     for (const k of Object.keys(obj)) {
       if (k === 'sort') continue // Exception
 
-      if (obj[k] !== undefined) acc++
+      if (obj[k] !== undefined && obj[k] !== '') acc++
     }
 
     return acc
index 8220a990be23c6e0dd1ac6db33eab1a6f2619ef3..07fb2c04877b0359159b387b9a7b4d9f68d874a5 100644 (file)
@@ -3,7 +3,12 @@
   <div class="row">
     <div class="col-lg-4 col-md-6 col-xs-12">
       <div class="form-group">
-        <div i18n class="radio-label">Sort</div>
+        <div class="radio-label label-container">
+          <label i18n>Sort</label>
+          <button i18n class="reset-button reset-button-small" (click)="resetField('sort', '-match')" *ngIf="advancedSearch.sort !== '-match'">
+            Reset
+          </button>
+        </div>
 
         <div class="peertube-radio-container" *ngFor="let sort of sorts">
           <input type="radio" name="sort" [id]="sort.id" [value]="sort.id" [(ngModel)]="advancedSearch.sort">
       </div>
 
       <div class="form-group">
-        <div i18n class="radio-label">Published date</div>
+        <div class="radio-label label-container">
+          <label i18n>Published date</label>
+          <button i18n class="reset-button reset-button-small" (click)="resetLocalField('publishedDateRange')" *ngIf="publishedDateRange !== undefined">
+            Reset
+          </button>
+        </div>
 
         <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
-          <input type="radio" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
+          <input type="radio" (change)="inputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
           <label [for]="date.id" class="radio">{{ date.label }}</label>
         </div>
       </div>
 
       <div class="form-group">
-        <label i18n for="original-publication-after">Original publication year</label>
+        <div class="label-container">
+          <label i18n for="original-publication-after">Original publication year</label>
+          <button i18n class="reset-button reset-button-small" (click)="resetOriginalPublicationYears()" *ngIf="originallyPublishedStartYear || originallyPublishedEndYear">
+            Reset
+          </button>
+        </div>
 
         <div class="row">
           <div class="col-sm-6">
             <input
+              (change)="inputUpdated()"
+              (keydown.enter)="$event.preventDefault()"
               type="text" id="original-publication-after" name="original-publication-after"
               i18n-placeholder placeholder="After..."
               [(ngModel)]="originallyPublishedStartYear"
@@ -33,6 +50,8 @@
           </div>
           <div class="col-sm-6">
             <input
+              (change)="inputUpdated()"
+              (keydown.enter)="$event.preventDefault()"
               type="text" id="original-publication-before" name="original-publication-before"
               i18n-placeholder placeholder="Before..."
               [(ngModel)]="originallyPublishedEndYear"
       </div>
 
       <div class="form-group">
-        <div i18n class="radio-label">Duration</div>
+        <div class="radio-label label-container">
+          <label i18n>Duration</label>
+          <button i18n class="reset-button reset-button-small" (click)="resetLocalField('durationRange')" *ngIf="durationRange !== undefined">
+            Reset
+          </button>
+        </div>
 
         <div class="peertube-radio-container" *ngFor="let duration of durationRanges">
-          <input type="radio" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
+          <input type="radio" (change)="inputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
           <label [for]="duration.id" class="radio">{{ duration.label }}</label>
         </div>
       </div>
 
       <div class="form-group">
-        <div i18n class="radio-label">Display sensitive content</div>
+        <div class="radio-label label-container">
+          <label i18n>Display sensitive content</label>
+          <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined">
+            Reset
+          </button>
+        </div>
 
         <div class="peertube-radio-container">
           <input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw">
     <div class="col-lg-4 col-md-6 col-xs-12">
       <div class="form-group">
         <label i18n for="category">Category</label>
+        <button i18n class="reset-button reset-button-small" (click)="resetField('categoryOneOf')" *ngIf="advancedSearch.categoryOneOf !== undefined">
+          Reset
+        </button>
         <div class="peertube-select-container">
           <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf">
-            <option></option>
+            <option [value]="undefined" i18n>Any or no category set</option>
             <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
           </select>
         </div>
 
       <div class="form-group">
         <label i18n for="licence">Licence</label>
+        <button i18n class="reset-button reset-button-small" (click)="resetField('licenceOneOf')" *ngIf="advancedSearch.licenceOneOf !== undefined">
+          Reset
+        </button>
         <div class="peertube-select-container">
           <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf">
-            <option></option>
+            <option [value]="undefined" i18n>Any or no license set</option>
             <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
           </select>
         </div>
 
       <div class="form-group">
         <label i18n for="language">Language</label>
+        <button i18n class="reset-button reset-button-small" (click)="resetField('languageOneOf')" *ngIf="advancedSearch.languageOneOf !== undefined">
+          Reset
+        </button>
         <div class="peertube-select-container">
           <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf">
-            <option></option>
+            <option [value]="undefined" i18n>Any or no language set</option>
             <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
           </select>
         </div>
     <div class="col-lg-4 col-md-6 col-xs-12">
       <div class="form-group">
         <label i18n for="tagsAllOf">All of these tags</label>
-        <input type="text" name="tagsAllOf" id="tagsAllOf" [(ngModel)]="advancedSearch.tagsAllOf" />
+        <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf">
+          Reset
+        </button>
+        <tag-input
+          [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf"
+          [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+          i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
+          maxItems="5" modelAsStrings="true"
+        ></tag-input>
       </div>
 
       <div class="form-group">
         <label i18n for="tagsOneOf">One of these tags</label>
-        <input type="text" name="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf" />
+        <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf">
+          Reset
+        </button>
+        <tag-input
+          [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf"
+          [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+          i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
+          maxItems="5" modelAsStrings="true"
+        ></tag-input>
       </div>
     </div>
   </div>
 
   <div class="submit-button">
+    <button i18n class="reset-button" (click)="reset()" *ngIf="advancedSearch.size()">
+      Reset
+    </button>
+
     <input type="submit" i18n-value value="Filter">
   </div>
 </form>
index cfc48fbef3cd263ef19a8d0a7a879c702de5b2b2..99af2e4c52931227c22f6dfc3693eb8edb6f20e0 100644 (file)
@@ -19,6 +19,8 @@ form {
 
 .peertube-select-container {
   @include peertube-select-container(auto);
+
+  margin-bottom: 1rem;
 }
 
 .form-group {
@@ -37,4 +39,92 @@ input[type=submit] {
 
 .submit-button {
   text-align: right;
-}
\ No newline at end of file
+}
+
+.reset-button {
+  @include peertube-button;
+
+  font-weight: $font-semibold;
+  display: inline-block;
+  padding: 0 10px 0 10px;
+  white-space: nowrap;
+  background: transparent;
+
+  margin-right: 1rem;
+}
+
+.reset-button-small {
+  font-size: 80%;
+  height: unset;
+  line-height: unset;
+  margin: unset;
+  margin-bottom: 0.5rem;
+}
+
+.label-container {
+  display: flex;
+  white-space: nowrap;
+}
+
+::ng-deep {
+  .ng2-tag-input {
+    border: none !important;
+  }
+
+  .ng2-tags-container {
+    display: flex;
+    align-items: center;
+    border: 1px solid #C6C6C6;
+    border-radius: 3px;
+    padding: 5px !important;
+    height: max-content;
+  }
+
+  tag-input-form {
+    input {
+      height: 30px !important;
+      font-size: 12px !important;
+
+      background-color: var(--mainBackgroundColor) !important;
+      color: var(--mainForegroundColor) !important;
+    }
+  }
+
+  tag {
+    background-color: $grey-background-color !important;
+    color: #000 !important;
+    border-radius: 3px !important;
+    font-size: 12px !important;
+    height: 30px !important;
+    line-height: 30px !important;
+    margin: 0 5px 0 0 !important;
+    cursor: default !important;
+    padding: 0 8px 0 10px !important;
+
+    div {
+      height: 100% !important;
+    }
+  }
+
+  delete-icon {
+    cursor: pointer !important;
+    height: auto !important;
+    vertical-align: middle !important;
+    padding-left: 6px !important;
+
+    svg {
+      position: relative;
+      top: -1px;
+      height: auto !important;
+      vertical-align: middle !important;
+
+      path  {
+        fill: $grey-foreground-color !important;
+      }
+    }
+
+    &:hover {
+      transform: none !important;
+    }
+  }
+}
index 14a05b721e7cd2e5ac94730eedd11fdf5451d621..b64c965b1bc9ee4fc9d69a04502149d6ffddc94f 100644 (file)
@@ -1,4 +1,6 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
+import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core'
+import { ValidatorFn } from '@angular/forms'
+import { VideoValidatorsService } from '@app/shared'
 import { ServerService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { AdvancedSearch } from '@app/search/advanced-search.model'
@@ -18,6 +20,9 @@ export class SearchFiltersComponent implements OnInit {
   videoLicences: VideoConstant<number>[] = []
   videoLanguages: VideoConstant<string>[] = []
 
+  tagValidators: ValidatorFn[]
+  tagValidatorsMessages: { [ name: string ]: string }
+
   publishedDateRanges: { id: string, label: string }[] = []
   sorts: { id: string, label: string }[] = []
   durationRanges: { id: string, label: string }[] = []
@@ -30,9 +35,16 @@ export class SearchFiltersComponent implements OnInit {
 
   constructor (
     private i18n: I18n,
+    private videoValidatorsService: VideoValidatorsService,
     private serverService: ServerService
   ) {
+    this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
+    this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
     this.publishedDateRanges = [
+      {
+        id: undefined,
+        label: this.i18n('Any')
+      },
       {
         id: 'today',
         label: this.i18n('Today')
@@ -52,6 +64,10 @@ export class SearchFiltersComponent implements OnInit {
     ]
 
     this.durationRanges = [
+      {
+        id: undefined,
+        label: this.i18n('Any')
+      },
       {
         id: 'short',
         label: this.i18n('Short (< 4 min)')
@@ -92,11 +108,14 @@ export class SearchFiltersComponent implements OnInit {
     this.loadOriginallyPublishedAtYears()
   }
 
-  formUpdated () {
+  inputUpdated () {
     this.updateModelFromDurationRange()
     this.updateModelFromPublishedRange()
     this.updateModelFromOriginallyPublishedAtYears()
+  }
 
+  formUpdated () {
+    this.inputUpdated()
     this.filtered.emit(this.advancedSearch)
   }
 
@@ -216,4 +235,26 @@ export class SearchFiltersComponent implements OnInit {
     this.advancedSearch.startDate = date.toISOString()
   }
 
+  private reset () {
+    this.advancedSearch.reset()
+    this.durationRange = undefined
+    this.publishedDateRange = undefined
+    this.originallyPublishedStartYear = undefined
+    this.originallyPublishedEndYear = undefined
+    this.inputUpdated()
+  }
+
+  private resetField (fieldName: string, value?: any) {
+    this.advancedSearch[fieldName] = value
+  }
+
+  private resetLocalField (fieldName: string, value?: any) {
+    this[fieldName] = value
+    this.inputUpdated()
+  }
+
+  private resetOriginalPublicationYears () {
+    this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined
+  }
+
 }
index 8b791621e09f4984d20664bf44fac55e477388aa..3b0fd6ee2a603a1662ebbbee5e43d83e2331c448 100644 (file)
@@ -1,4 +1,5 @@
 import { NgModule } from '@angular/core'
+import { TagInputModule } from 'ngx-chips'
 import { SharedModule } from '../shared'
 import { SearchComponent } from '@app/search/search.component'
 import { SearchService } from '@app/search/search.service'
@@ -7,6 +8,8 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component'
 
 @NgModule({
   imports: [
+    TagInputModule,
+
     SearchRoutingModule,
     SharedModule
   ],
@@ -17,6 +20,7 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component'
   ],
 
   exports: [
+    TagInputModule,
     SearchComponent
   ],
 
index 9479c588acb31ee411fdc5a4c0ff113edad0c5d1..3d57e9152aca084157fe1796f879f141f567feaa 100644 (file)
@@ -150,12 +150,13 @@ p-calendar {
     border: 1px solid #C6C6C6;
     border-radius: 3px;
     padding: 5px !important;
-    height: 40px;
+    height: max-content;
   }
 
   tag-input-form {
     input {
       height: 30px !important;
+      font-size: 12px !important;
 
       background-color: var(--mainBackgroundColor) !important;
       color: var(--mainForegroundColor) !important;
@@ -166,7 +167,7 @@ p-calendar {
     background-color: $grey-background-color !important;
     color: #000 !important;
     border-radius: 3px !important;
-    font-size: 15px !important;
+    font-size: 12px !important;
     height: 30px !important;
     line-height: 30px !important;
     margin: 0 5px 0 0 !important;
index 56126d41f9e91950bf87b01a5f7b15374c6dadd1..4a1780b3f8ecbcc6cc9151b69da36f4392c13332 100644 (file)
@@ -205,6 +205,7 @@ $play-overlay-width: 18px;
     color: $grey-foreground-color;
     margin-bottom: 10px;
     font-weight: $font-semibold;
+    text-decoration: none;
   }
 
   @media screen and (max-width: $mobile-view) {