search filtering improvements per #1654

This commit is contained in:
Rigel Kent 2019-12-04 17:12:23 +01:00 committed by Chocobozzz
parent fee47735bd
commit 2526690866
7 changed files with 214 additions and 19 deletions

View File

@ -65,7 +65,7 @@ export class AdvancedSearch {
for (const k of Object.keys(obj)) { for (const k of Object.keys(obj)) {
if (k === 'sort') continue // Exception if (k === 'sort') continue // Exception
if (obj[k] !== undefined) return true if (obj[k] !== undefined && obj[k] !== '') return true
} }
return false return false
@ -131,7 +131,7 @@ export class AdvancedSearch {
for (const k of Object.keys(obj)) { for (const k of Object.keys(obj)) {
if (k === 'sort') continue // Exception if (k === 'sort') continue // Exception
if (obj[k] !== undefined) acc++ if (obj[k] !== undefined && obj[k] !== '') acc++
} }
return acc return acc

View File

@ -3,7 +3,12 @@
<div class="row"> <div class="row">
<div class="col-lg-4 col-md-6 col-xs-12"> <div class="col-lg-4 col-md-6 col-xs-12">
<div class="form-group"> <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"> <div class="peertube-radio-container" *ngFor="let sort of sorts">
<input type="radio" name="sort" [id]="sort.id" [value]="sort.id" [(ngModel)]="advancedSearch.sort"> <input type="radio" name="sort" [id]="sort.id" [value]="sort.id" [(ngModel)]="advancedSearch.sort">
@ -12,20 +17,32 @@
</div> </div>
<div class="form-group"> <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"> <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> <label [for]="date.id" class="radio">{{ date.label }}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-container">
<label i18n for="original-publication-after">Original publication year</label> <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="row">
<div class="col-sm-6"> <div class="col-sm-6">
<input <input
(change)="inputUpdated()"
(keydown.enter)="$event.preventDefault()"
type="text" id="original-publication-after" name="original-publication-after" type="text" id="original-publication-after" name="original-publication-after"
i18n-placeholder placeholder="After..." i18n-placeholder placeholder="After..."
[(ngModel)]="originallyPublishedStartYear" [(ngModel)]="originallyPublishedStartYear"
@ -33,6 +50,8 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<input <input
(change)="inputUpdated()"
(keydown.enter)="$event.preventDefault()"
type="text" id="original-publication-before" name="original-publication-before" type="text" id="original-publication-before" name="original-publication-before"
i18n-placeholder placeholder="Before..." i18n-placeholder placeholder="Before..."
[(ngModel)]="originallyPublishedEndYear" [(ngModel)]="originallyPublishedEndYear"
@ -42,16 +61,26 @@
</div> </div>
<div class="form-group"> <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"> <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> <label [for]="duration.id" class="radio">{{ duration.label }}</label>
</div> </div>
</div> </div>
<div class="form-group"> <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"> <div class="peertube-radio-container">
<input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw"> <input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw">
@ -69,9 +98,12 @@
<div class="col-lg-4 col-md-6 col-xs-12"> <div class="col-lg-4 col-md-6 col-xs-12">
<div class="form-group"> <div class="form-group">
<label i18n for="category">Category</label> <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"> <div class="peertube-select-container">
<select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf"> <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> <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
</select> </select>
</div> </div>
@ -79,9 +111,12 @@
<div class="form-group"> <div class="form-group">
<label i18n for="licence">Licence</label> <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"> <div class="peertube-select-container">
<select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf"> <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> <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
</select> </select>
</div> </div>
@ -89,9 +124,12 @@
<div class="form-group"> <div class="form-group">
<label i18n for="language">Language</label> <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"> <div class="peertube-select-container">
<select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf"> <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> <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
</select> </select>
</div> </div>
@ -101,17 +139,37 @@
<div class="col-lg-4 col-md-6 col-xs-12"> <div class="col-lg-4 col-md-6 col-xs-12">
<div class="form-group"> <div class="form-group">
<label i18n for="tagsAllOf">All of these tags</label> <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>
<div class="form-group"> <div class="form-group">
<label i18n for="tagsOneOf">One of these tags</label> <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>
</div> </div>
<div class="submit-button"> <div class="submit-button">
<button i18n class="reset-button" (click)="reset()" *ngIf="advancedSearch.size()">
Reset
</button>
<input type="submit" i18n-value value="Filter"> <input type="submit" i18n-value value="Filter">
</div> </div>
</form> </form>

View File

@ -19,6 +19,8 @@ form {
.peertube-select-container { .peertube-select-container {
@include peertube-select-container(auto); @include peertube-select-container(auto);
margin-bottom: 1rem;
} }
.form-group { .form-group {
@ -38,3 +40,91 @@ input[type=submit] {
.submit-button { .submit-button {
text-align: right; text-align: right;
} }
.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;
}
}
}

View 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 { ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { AdvancedSearch } from '@app/search/advanced-search.model' import { AdvancedSearch } from '@app/search/advanced-search.model'
@ -18,6 +20,9 @@ export class SearchFiltersComponent implements OnInit {
videoLicences: VideoConstant<number>[] = [] videoLicences: VideoConstant<number>[] = []
videoLanguages: VideoConstant<string>[] = [] videoLanguages: VideoConstant<string>[] = []
tagValidators: ValidatorFn[]
tagValidatorsMessages: { [ name: string ]: string }
publishedDateRanges: { id: string, label: string }[] = [] publishedDateRanges: { id: string, label: string }[] = []
sorts: { id: string, label: string }[] = [] sorts: { id: string, label: string }[] = []
durationRanges: { id: string, label: string }[] = [] durationRanges: { id: string, label: string }[] = []
@ -30,9 +35,16 @@ export class SearchFiltersComponent implements OnInit {
constructor ( constructor (
private i18n: I18n, private i18n: I18n,
private videoValidatorsService: VideoValidatorsService,
private serverService: ServerService private serverService: ServerService
) { ) {
this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
this.publishedDateRanges = [ this.publishedDateRanges = [
{
id: undefined,
label: this.i18n('Any')
},
{ {
id: 'today', id: 'today',
label: this.i18n('Today') label: this.i18n('Today')
@ -52,6 +64,10 @@ export class SearchFiltersComponent implements OnInit {
] ]
this.durationRanges = [ this.durationRanges = [
{
id: undefined,
label: this.i18n('Any')
},
{ {
id: 'short', id: 'short',
label: this.i18n('Short (< 4 min)') label: this.i18n('Short (< 4 min)')
@ -92,11 +108,14 @@ export class SearchFiltersComponent implements OnInit {
this.loadOriginallyPublishedAtYears() this.loadOriginallyPublishedAtYears()
} }
formUpdated () { inputUpdated () {
this.updateModelFromDurationRange() this.updateModelFromDurationRange()
this.updateModelFromPublishedRange() this.updateModelFromPublishedRange()
this.updateModelFromOriginallyPublishedAtYears() this.updateModelFromOriginallyPublishedAtYears()
}
formUpdated () {
this.inputUpdated()
this.filtered.emit(this.advancedSearch) this.filtered.emit(this.advancedSearch)
} }
@ -216,4 +235,26 @@ export class SearchFiltersComponent implements OnInit {
this.advancedSearch.startDate = date.toISOString() 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
}
} }

View File

@ -1,4 +1,5 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { TagInputModule } from 'ngx-chips'
import { SharedModule } from '../shared' import { SharedModule } from '../shared'
import { SearchComponent } from '@app/search/search.component' import { SearchComponent } from '@app/search/search.component'
import { SearchService } from '@app/search/search.service' import { SearchService } from '@app/search/search.service'
@ -7,6 +8,8 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component'
@NgModule({ @NgModule({
imports: [ imports: [
TagInputModule,
SearchRoutingModule, SearchRoutingModule,
SharedModule SharedModule
], ],
@ -17,6 +20,7 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component'
], ],
exports: [ exports: [
TagInputModule,
SearchComponent SearchComponent
], ],

View File

@ -150,12 +150,13 @@ p-calendar {
border: 1px solid #C6C6C6; border: 1px solid #C6C6C6;
border-radius: 3px; border-radius: 3px;
padding: 5px !important; padding: 5px !important;
height: 40px; height: max-content;
} }
tag-input-form { tag-input-form {
input { input {
height: 30px !important; height: 30px !important;
font-size: 12px !important;
background-color: var(--mainBackgroundColor) !important; background-color: var(--mainBackgroundColor) !important;
color: var(--mainForegroundColor) !important; color: var(--mainForegroundColor) !important;
@ -166,7 +167,7 @@ p-calendar {
background-color: $grey-background-color !important; background-color: $grey-background-color !important;
color: #000 !important; color: #000 !important;
border-radius: 3px !important; border-radius: 3px !important;
font-size: 15px !important; font-size: 12px !important;
height: 30px !important; height: 30px !important;
line-height: 30px !important; line-height: 30px !important;
margin: 0 5px 0 0 !important; margin: 0 5px 0 0 !important;

View File

@ -205,6 +205,7 @@ $play-overlay-width: 18px;
color: $grey-foreground-color; color: $grey-foreground-color;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: $font-semibold; font-weight: $font-semibold;
text-decoration: none;
} }
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {