1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-28 08:58:49 +02:00

Improve search UI #683

This commit is contained in:
Patrik J. Braun 2023-09-08 22:55:24 +02:00
parent b6b8170187
commit d31cf0040c
12 changed files with 150 additions and 54 deletions

View File

@ -41,7 +41,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
this.configStyles = enumToTranslatedArray(ConfigStyle);
const wc = WebConfigClassBuilder.attachPrivateInterface(new WebConfig());
this.configPaths = Object.keys(wc.State)
.filter(s => !wc.__state[s].volatile);
.filter(s => !wc.__state[s].volatile && s === 'Jobs');
}
ngAfterViewInit(): void {

View File

@ -2,6 +2,7 @@ import {Component, EventEmitter, forwardRef, Input, Output,} from '@angular/core
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../../../../../common/entities/SearchQueryDTO';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, ValidationErrors, Validator,} from '@angular/forms';
import {SearchQueryParserService} from '../search-query-parser.service';
import {Utils} from '../../../../../../common/Utils';
@Component({
selector: 'app-gallery-search-query-builder',
@ -34,10 +35,15 @@ export class GallerySearchQueryBuilderComponent
}
validateRawSearchText(): void {
try {
this.searchQueryDTO = this.searchQueryParserService.parse(
const newDTO = this.searchQueryParserService.parse(
this.rawSearchText
);
if (Utils.equalsFilter(this.searchQueryDTO, newDTO)) {
return;
}
this.searchQueryDTO = newDTO;
this.onChange();
} catch (e) {
console.error(e);
@ -61,6 +67,17 @@ export class GallerySearchQueryBuilderComponent
}
public writeValue(obj: any): void {
try {
// do not trigger change if nothing changed
if (Utils.equalsFilter(this.searchQueryDTO, obj) &&
Utils.equalsFilter(this.searchQueryParserService.parse(
this.rawSearchText
), obj)) {
return;
}
}catch (e) {
// if cant parse they are not the same
}
this.searchQueryDTO = obj;
this.rawSearchText = this.searchQueryParserService.stringify(
this.searchQueryDTO
@ -76,6 +93,14 @@ export class GallerySearchQueryBuilderComponent
}
public onChange(): void {
try {
if (Utils.equalsFilter(this.searchQueryParserService.parse(this.rawSearchText), this.searchQueryDTO)) {
this.propagateChange(this.searchQueryDTO);
return;
}
}catch (e) {
// if cant parse they are not the same
}
this.rawSearchText = this.searchQueryParserService.stringify(
this.searchQueryDTO
);

View File

@ -2,8 +2,8 @@
<ng-container *ngIf="IsListQuery">
<div class="col-md-3 col-4">
<select
id="listSearchType"
name="listSearchType"
[id]="'listSearchType'+id"
[name]="'listSearchType'+id"
class="form-select"
[(ngModel)]="queryEntry.type"
(ngModelChange)="onChangeType()">
@ -12,7 +12,7 @@
</select>
</div>
<ng-container *ngIf="queryEntry.type == SearchQueryTypes.SOME_OF">
<label class="col-4 col-sm-auto control-label" for="someOfMinValue">
<label class="col-4 col-sm-auto control-label" [for]="'someOfMinValue'+id">
<ng-container i18n>At least this many</ng-container>
(1-{{AsListQuery.list.length}}):</label>
<div class="col-md col">
@ -23,8 +23,8 @@
i18n-title
[(ngModel)]="AsSomeOfQuery.min"
(ngModelChange)="onChange()"
name="someOfMinValue"
id="someOfMinValue"
[name]="'someOfMinValue'+id"
[id]="'someOfMinValue'+id"
required="required">
</div>
</ng-container>
@ -40,6 +40,7 @@
</button>
<div class="container query-list">
<app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i"
[id]="id+'_'+i"
[(ngModel)]="AsListQuery.list[i]"
(delete)="itemDeleted(i)">
</app-gallery-search-query-entry>
@ -49,7 +50,7 @@
<ng-icon
class="me-1"
style="margin-left: -0.1em; margin-right: -0.1em"
name="ionAddOutline"
[name]="'ionAddOutline'+id"
title="Add" i18n-title></ng-icon>
<span i18n>Add</span>
</button>
@ -59,8 +60,8 @@
<div class="col-lg-4 col-xl-3">
<div class="input-group">
<select
id="searchType"
name="searchType"
[id]="'searchType'+id"
[name]="'searchType'+id"
class="form-select"
[(ngModel)]="queryEntry.type"
(ngModelChange)="onChangeType()">
@ -68,11 +69,11 @@
</option>
</select>
<select
id="negate"
name="negate"
[id]="'negate'+id"
[name]="'negate'+id"
class="form-select w-auto flex-grow-0"
title="Negate"
p18n-title
i18n-title
[(ngModel)]="SelectedMatchType"
(ngModelChange)="onChange()">
<option *ngFor="let mt of MatchingTypes" [ngValue]="mt">{{mt}}
@ -82,8 +83,8 @@
<div class="col-10 col-lg" *ngIf="IsTextQuery">
<div class="input-group">
<input
id="searchField"
name="searchField"
[id]="'searchField'+id"
[name]="'searchField'+id"
placeholder="Search text"
i18n-placeholder
class="form-control rounded-2"
@ -104,15 +105,15 @@
step="0.1"
[(ngModel)]="AsDistanceQuery.distance"
(ngModelChange)="onChange()"
name="distance" required>
[name]="'distance'+id" required>
<span class="input-group-text">km</span>
</div>
</div>
<div class="col-md-8">
<div class="input-group">
<label class="control-label me-2" for="maxResolution">From</label>
<input id="from"
name="from"
<label class="control-label me-2" [for]="'maxResolution_'+id">From</label>
<input [name]="'maxResolution_'+id"
[id]="'maxResolution_'+id"
title="From"
placeholder="New York"
i18n-title
@ -132,13 +133,14 @@
i18n-title
[ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
(ngModelChange)="AsRangeQuery.value = $event; onChange() "
[value]="AsRangeQuery.value | date:'yyyy-MM-dd'" #from_date="ngModel"
[value]="AsRangeQuery.value | date:'yyyy-MM-dd'"
#from_date="ngModel"
class="form-control input-md rounded-2"
type="date">
</div>
<div *ngSwitchCase="SearchQueryTypes.to_date" class="col-10 col-lg d-flex">
<input id="to_date"
name="to_date"
<input [id]="'to_date'+id"
[name]="'to_date'+id"
title="To date"
i18n-title
[ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
@ -148,8 +150,8 @@
type="date">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_rating" class="col-10 col-lg d-flex">
<input id="minRating"
name="minRating"
<input [id]="'minRating'+id"
[name]="'minRating'+id"
title="Minimum Rating"
placeholder="0"
i18n-title
@ -161,8 +163,8 @@
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.max_rating" class="col-10 col-lg d-flex">
<input id="maxRating"
name="maxRating"
<input [id]="'maxRating'+id"
[name]="'maxRating'+id"
title="Maximum Rating"
placeholder="5"
i18n-title
@ -174,8 +176,8 @@
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_person_count" class="col-10 col-lg d-flex">
<input id="min_person_count"
name="min_person_count"
<input [id]="'min_person_count'+id"
[name]="'min_person_count'+id"
title="Minimum Person count"
placeholder="0"
i18n-title
@ -187,8 +189,8 @@
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.max_person_count" class="col-10 col-lg d-flex">
<input id="max_person_count"
name="max_person_count"
<input [id]="'max_person_count'+id"
[name]="'max_person_count'+id"
title="Maximum Person count"
placeholder="5"
i18n-title
@ -201,8 +203,8 @@
</div>
<div *ngSwitchCase="SearchQueryTypes.min_resolution" class="col-10 col-lg">
<div class="input-group">
<input id="minResolution"
name="minResolution"
<input [id]="'minResolution'+id"
[name]="'minResolution'+id"
title="Minimum Resolution"
placeholder="0"
i18n-title
@ -217,8 +219,8 @@
<div *ngSwitchCase="SearchQueryTypes.max_resolution" class="col-10 col-lg">
<div class="input-group">
<input id="maxResolution"
name="maxResolution"
<input [id]="'maxResolution'+id"
[name]="'maxResolution'+id"
title="Maximum Resolution"
placeholder="5"
i18n-title
@ -232,11 +234,11 @@
</div>
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-10 col-lg d-flex">
<div class="input-group col-md-6">
<select class="form-select rounded-2"
<select [id]="'orientation-select'+id"
[name]="'orientation-select'+id"
class="form-select rounded-2"
[(ngModel)]="AsOrientationQuery.landscape"
(ngModelChange)="onChange()"
id="orientation-select"
name="orientation-select"
title="Orientation"
required>
<option [ngValue]="true" i18n>Landscape</option>
@ -249,8 +251,8 @@
<div class="row">
<div class="input-group col-md-6">
<span class="input-group-text" i18n>Last</span>
<input id="daysLength"
name="daysLength"
<input [id]="'daysLength'+id"
[name]="'daysLength'+id"
title="Last N Days"
placeholder="1"
i18n-title
@ -265,8 +267,8 @@
<div class="input-group col-md-6">
<input
*ngIf="AsDatePatternQuery.frequency == DatePatternFrequency.days_ago || AsDatePatternQuery.frequency == DatePatternFrequency.weeks_ago || AsDatePatternQuery.frequency == DatePatternFrequency.months_ago || AsDatePatternQuery.frequency == DatePatternFrequency.years_ago"
id="agoNumber"
name="agoNumber"
[id]="'agoNumber'+id"
[name]="'agoNumber'+id"
title="Ago"
placeholder="1"
i18n-title
@ -278,8 +280,8 @@
<select class="form-select rounded-2"
[(ngModel)]="AsDatePatternQuery.frequency"
(ngModelChange)="onChange()"
id="date_pattern-select"
name="date_pattern-select"
[id]="'date_pattern-select'+id"
[name]="'date_pattern-select'+id"
title="Date Pattern"
required>
<option [ngValue]="DatePatternFrequency.days_ago" i18n>Day(s) ago</option>

View File

@ -1,4 +1,4 @@
import {Component, EventEmitter, forwardRef, Output} from '@angular/core';
import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import {
DatePatternFrequency,
DatePatternSearch,
@ -42,6 +42,7 @@ export class GallerySearchQueryEntryComponent
public DatePatternFrequency = DatePatternFrequency;
public TextSearchQueryMatchTypes = TextSearchQueryMatchTypes;
@Output() delete = new EventEmitter<void>();
@Input() id = 'NA';
constructor() {
this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes);

View File

@ -4,6 +4,7 @@
[placeholder]="placeholder"
(keyup)="onSearchChange($event)"
(blur)="onFocusLost()"
(focus)="onFocus()"
[(ngModel)]="rawSearchText"
(ngModelChange)="onChange()"
(keydown.enter)="OnEnter($event)"

View File

@ -49,6 +49,7 @@ export class GallerySearchFieldBaseComponent
};
private autoCompleteItemsSubscription: Subscription = null;
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
inFocus: boolean;
constructor(
private autoCompleteService: AutoCompleteService,
@ -68,6 +69,7 @@ export class GallerySearchFieldBaseComponent
return '';
}
if (
!this.inFocus ||
!this.autoCompleteItems ||
!this.autoCompleteItems.value ||
this.autoCompleteItems.value.length === 0
@ -130,7 +132,12 @@ export class GallerySearchFieldBaseComponent
this.mouseOverAutoComplete = value;
}
onFocus(): void {
this.inFocus = true;
}
public onFocusLost(): void {
this.inFocus = false;
if (this.mouseOverAutoComplete === false) {
this.autoCompleteRenders = [];
}
@ -160,6 +167,7 @@ export class GallerySearchFieldBaseComponent
0,
this.rawSearchText.length - token.current.length
) + item.queryHint;
console.log('aa');
this.onChange();
this.emptyAutoComplete();
}
@ -299,5 +307,6 @@ export class GallerySearchFieldBaseComponent
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
private propagateTouch = (_: never): void => {
};
}

View File

@ -29,7 +29,7 @@
name="search-query-builder"
[(ngModel)]="searchQueryDTO"
[placeholder]="placeholder"
(change)="onQueryChange()"
(ngModelChange)="onQueryChange()"
(search)="search.emit()">
</app-gallery-search-query-builder>

View File

@ -1,10 +1,11 @@
import {Component, EventEmitter, forwardRef, Input, Output,TemplateRef} from '@angular/core';
import {Component, EventEmitter, forwardRef, Input, Output, TemplateRef} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {AutoCompleteService} from '../autocomplete.service';
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
import {ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, ValidationErrors, Validator,} from '@angular/forms';
import {SearchQueryParserService} from '../search-query-parser.service';
import {BsModalRef, BsModalService,} from '../../../../../../../node_modules/ngx-bootstrap/modal';
import {Utils} from '../../../../../../common/Utils';
@Component({
selector: 'app-gallery-search-field',
@ -58,6 +59,16 @@ export class GallerySearchFieldComponent
}
public writeValue(obj: SearchQueryDTO): void {
try {
if (Utils.equalsFilter(this.searchQueryDTO, obj) &&
Utils.equalsFilter(this.searchQueryParserService.parse(
this.rawSearchText
), obj)) {
return;
}
} catch (e) {
// if cant parse they are not the same
}
this.searchQueryDTO = obj;
this.rawSearchText = this.searchQueryParserService.stringify(
this.searchQueryDTO
@ -81,9 +92,18 @@ export class GallerySearchFieldComponent
}
onQueryChange(): void {
try {
if (Utils.equalsFilter(this.searchQueryParserService.parse(this.rawSearchText), this.searchQueryDTO)) {
this.onChange();
return;
}
} catch (e) {
// if cant parse they are not the same
}
this.rawSearchText = this.searchQueryParserService.stringify(
this.searchQueryDTO
);
this.onChange();
}

View File

@ -34,7 +34,7 @@
<app-gallery-search-query-builder
name="search-query-builder"
[(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
(ngModelChange)="onQueryChange()"
(search)="Search()">
</app-gallery-search-query-builder>

View File

@ -11,6 +11,7 @@ import {AlbumsService} from '../../albums/albums.service';
import {Config} from '../../../../../common/config/public/Config';
import {UserRoles} from '../../../../../common/entities/UserDTO';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {Utils} from '../../../../../common/Utils';
@Component({
selector: 'app-gallery-search',
@ -102,11 +103,14 @@ export class GallerySearchComponent implements OnDestroy {
this.saveSearchModalRef = null;
}
onQueryChange(): void {
public onQueryChange(): void {
if (Utils.equalsFilter(this.searchQueryParserService.parse(this.rawSearchText), this.searchQueryDTO)) {
return;
}
this.rawSearchText = this.searchQueryParserService.stringify(
this.searchQueryDTO
);
// this.validateRawSearchText();
}
validateRawSearchText(): void {

View File

@ -41,12 +41,13 @@
<div class="col-md-12">
<div class="mb-1 row">
<label class="col-md-2 control-label" [for]="'jobName'+i" i18n>Job:</label>
<label class="col-md-2 control-label" i18n>Job:</label>
<div class="col-md-4">
{{backendTextService.getJobName(schedule.jobName)}}
</div>
<div class="col-md-6">
<app-settings-job-button class="float-end"
[id]="'jobName'+i"
[jobName]="schedule.jobName"
[allowParallelRun]="schedule.allowParallelRun"
(jobError)="error=$event"
@ -59,7 +60,9 @@
<div class="col-md-10">
<select class="form-select" [(ngModel)]="schedule.trigger.type"
(ngModelChange)="jobTriggerTypeChanged($event,schedule); onChange($event);"
[name]="'repeatType'+i" required>
[name]="'repeatType'+i"
[id]="'repeatType'+i"
required>
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
[ngValue]="jobTrigger.key">{{jobTrigger.value}}
</option>
@ -78,7 +81,8 @@
<select class="form-select"
[(ngModel)]="schedule.trigger.afterScheduleName"
(ngModelChange)="onChange($event)"
[name]="'triggerAfter'+i" required>
[name]="'triggerAfter'+i"
[id]="'triggerAfter'+i" required>
<ng-container *ngFor="let sch of sortedSchedules">
<option *ngIf="sch.name !== schedule.name"
[ngValue]="sch.name">{{sch.name}}
@ -99,6 +103,7 @@
<div class="col-md-10">
<app-timestamp-datepicker
[name]="'triggerTime'+i"
[id]="'triggerTime'+i"
(timestampChange)="onChange($event)"
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
</div>
@ -114,6 +119,7 @@
[(ngModel)]="schedule.trigger.periodicity"
(ngModelChange)="onChange($event)"
[name]="'periodicity' + i"
[id]="'periodicity' + i"
required>
<option *ngFor="let period of periods; let i = index"
[ngValue]="i">
@ -292,7 +298,7 @@
<div class="mb-1 row"
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i" i18n>Pick</label>
[for]="configEntry.id+'_'+i+'_'+j" i18n>Pick</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control" [name]="configEntry.id+'_'+i+'_'+j"

View File

@ -0,0 +1,28 @@
describe('Search', () => {
beforeEach(() => {
cy.visit('/');
cy.get('.card-body');
cy.get('.col-sm-12').contains('Login');
/* ==== Generated with Cypress Studio ==== */
cy.get('#username').type('admin');
cy.get('#password').clear();
cy.get('#password').type('admin');
cy.intercept({
method: 'Get',
url: '/pgapi/gallery/content/',
}).as('getContent');
cy.get('.col-sm-12 > .btn').click();
});
it('Search builder should propagate to search bar', () => {
cy.get('.mb-0 > :nth-child(1) > .nav-link').contains('Gallery');
cy.get('app-gallery-search .search-text').type('a and b', {force: true});
cy.get('app-gallery-search ng-icon[name="ionChevronDownOutline"]').click();
cy.get('app-gallery-search-query-builder app-gallery-search-field-base input.search-text').should('have.value', 'a and b');
cy.get('app-gallery-search-query-entry .btn-danger').last().click();
cy.get('app-gallery-search-query-builder app-gallery-search-field-base input.search-text').should('have.value', 'a');
cy.get('modal-container .btn-close').click();
cy.get('app-gallery-search .search-text').should('have.value', 'a');
});
});