Saves the value of the local search in 'list-view-table'

- Builds a new component 'rounded-input' to have a input-field with rounded borders.
- Saves the input on every change in the local storage.
- In the `OnInit`-function this value is restored.
This commit is contained in:
GabrielMeyer 2019-07-29 16:15:47 +02:00
parent 60098af22d
commit 2a1a44ee5a
16 changed files with 337 additions and 64 deletions

View File

@ -4,6 +4,7 @@
[filterCount]="countFilter" [filterCount]="countFilter"
[filterService]="filterService" [filterService]="filterService"
[sortService]="sortService" [sortService]="sortService"
[searchFieldInput]="inputValue"
(searchFieldChange)="searchFilter($event)" (searchFieldChange)="searchFilter($event)"
> >
</os-sort-filter-bar> </os-sort-filter-bar>
@ -14,7 +15,7 @@
[ngClass]="{ [ngClass]="{
'virtual-scroll-with-head-bar ngrid-hide-head': showFilterBar, 'virtual-scroll-with-head-bar ngrid-hide-head': showFilterBar,
'virtual-scroll-full-page': !showFilterBar, 'virtual-scroll-full-page': !showFilterBar,
'multiselect': multiSelect multiselect: multiSelect
}" }"
cellTooltip cellTooltip
[showHeader]="!showFilterBar" [showHeader]="!showFilterBar"

View File

@ -64,7 +64,7 @@ export interface ColumnRestriction {
* [hiddenInMobile]="['state']" * [hiddenInMobile]="['state']"
* [allowProjector]="false" * [allowProjector]="false"
* [multiSelect]="isMultiSelect" * [multiSelect]="isMultiSelect"
* scrollKey="motion" * listStorageKey="motion"
* [(selectedRows)]="selectedRows" * [(selectedRows)]="selectedRows"
* (dataSourceChange)="onDataSourceChange($event)" * (dataSourceChange)="onDataSourceChange($event)"
* > * >
@ -157,7 +157,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
* Key to restore scroll position after navigating * Key to restore scroll position after navigating
*/ */
@Input() @Input()
public scrollKey: string; public listStorageKey: string;
/** /**
* Wether or not to show the filter bar * Wether or not to show the filter bar
@ -196,6 +196,12 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
*/ */
public inputValue: string; public inputValue: string;
/**
* Flag to indicate, whether the table is loading the first time or not.
* Otherwise the `DataSource` will be empty, if there is a query stored in the local-storage.
*/
private initialLoading = true;
/** /**
* Most, of not all list views require these * Most, of not all list views require these
*/ */
@ -365,8 +371,9 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
.build(); .build();
// restore scroll position // restore scroll position
if (this.scrollKey) { if (this.listStorageKey) {
this.scrollToPreviousPosition(this.scrollKey); this.scrollToPreviousPosition(this.listStorageKey);
this.restoreSearchQuery(this.listStorageKey);
} }
} }
@ -409,9 +416,16 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
* @param event the string to search for * @param event the string to search for
*/ */
public searchFilter(filterValue: string): void { public searchFilter(filterValue: string): void {
if (this.listStorageKey) {
this.saveSearchQuery(this.listStorageKey, filterValue);
}
this.inputValue = filterValue; this.inputValue = filterValue;
if (this.initialLoading) {
this.initialLoading = false;
} else {
this.dataSource.syncFilter(); this.dataSource.syncFilter();
} }
}
/** /**
* Loads the scroll-index from the storage * Loads the scroll-index from the storage
@ -424,6 +438,25 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
return scrollIndex ? scrollIndex : 0; return scrollIndex ? scrollIndex : 0;
} }
/**
* Saves the given query to restore it later, if navigating to other sites happened.
*
* @param key The `StorageKey` for the list-view.
* @param query The query, that should be stored.
*/
public saveSearchQuery(key: string, query: string): void {
this.store.set(`query_${key}`, query);
}
/**
* Function to load any query from the store for the given `StorageKey`.
*
* @param key The `StorageKey` for the list-view.
*/
public async restoreSearchQuery(key: string): Promise<void> {
this.inputValue = await this.store.get<string>(`query_${key}`);
}
/** /**
* Automatically scrolls to a stored scroll position * Automatically scrolls to a stored scroll position
* *

View File

@ -0,0 +1,17 @@
<div class="input-container">
<div class="input-prefix">
<mat-icon color="primary">search</mat-icon>
</div>
<input
#osInput
[autofocus]="autofocus"
[placeholder]="placeholder"
class="rounded-input"
[ngClass]="[size]"
[formControl]="modelForm"
(keyup)="keyPressed($event)"
/>
<div *ngIf="modelForm.value !== ''" class="input-suffix">
<mat-icon (mouseup)="clear()">close</mat-icon>
</div>
</div>

View File

@ -0,0 +1,47 @@
.input-container {
position: relative;
height: 100%;
&,
div {
display: flex;
align-items: center;
z-index: 1;
}
div {
position: absolute;
&.input-prefix {
left: 8px;
}
&.input-suffix {
right: 8px;
color: #666;
}
}
.rounded-input {
outline: 0;
z-index: 0;
height: 24px;
width: 100%;
padding: 8px 39px;
border-radius: 32px;
font-size: 16px;
border: 1px solid #ccc;
color: #666;
transition: all 0.25s ease;
&.small {
height: 14px;
font-size: 14px;
width: 100px;
&:focus {
width: 200px;
}
}
}
mat-icon {
cursor: pointer;
}
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { RoundedInputComponent } from './rounded-input.component';
describe('RoundedInputComponent', () => {
let component: RoundedInputComponent;
let fixture: ComponentFixture<RoundedInputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RoundedInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,147 @@
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'os-rounded-input',
templateUrl: './rounded-input.component.html',
styleUrls: ['./rounded-input.component.scss']
})
export class RoundedInputComponent implements OnInit, OnDestroy {
/**
* Reference to the `<input />`-element.
*/
@ViewChild('osInput', { static: true })
public osInput: ElementRef;
/**
* Setter for the model. This could be useful, if the value of the input
* should be set from outside of this component.
*
* @param value The new value of the input.
*/
@Input()
public set model(value: string) {
if (!!value) {
this.modelForm.setValue(value);
}
}
/**
* Getter for the model.
*
* @returns {string} The value of the FormControl. If this is undefined or null, it returns an empty string.
*/
public get model(): string {
return this.modelForm ? this.modelForm.value : '';
}
/**
* Controls the size of the input.
*
* Possible values are `'small' | 'medium' | 'large'`.
* Defaults to `'medium'`.
*/
@Input()
public size: 'small' | 'medium' | 'large' = 'medium';
/**
* Custom `FormControl`.
*/
@Input()
public modelForm: FormControl;
/**
* Boolean, whether the input should be focussed automatically, if the component enters the DOM.
*/
@Input()
public autofocus = false;
/**
* Boolean, whether the input should fire the value-change-event after a specific time.
*/
@Input()
public lazyInput = false;
/**
* Placeholder for the input. Defaults to `Search...`.
*/
@Input()
public placeholder = 'Search...';
/**
* Boolean, whether the input will be cleared, if the user presses `Escape`.
*/
@Input()
public clearOnEscape = true;
/**
* EventHandler for the input-changes.
*/
@Output()
public oninput: EventEmitter<string> = new EventEmitter();
/**
* EventHandler for the key-events.
*/
@Output()
public onkeyup: EventEmitter<KeyboardEvent> = new EventEmitter();
/**
* Subscription, that will handle the value-changes of the input.
*/
private subscription: Subscription;
/**
* Default constructor
*/
public constructor() {
if (!this.modelForm) {
this.modelForm = new FormControl(this.model);
}
}
/**
* Overwrites `OnInit` - initializes the subscription.
*/
public ngOnInit(): void {
this.subscription = this.modelForm.valueChanges
.pipe(debounceTime(this.lazyInput ? 250 : 0))
.subscribe(nextValue => {
this.oninput.emit(nextValue);
});
}
/**
* Overwrites `OnDestroy` - clears the subscription.
*/
public ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
/**
* Function to clear the input and refocus it.
*/
public clear(): void {
this.osInput.nativeElement.focus();
this.modelForm.setValue('');
}
/**
* Function to handle typing.
* Useful to listen to special keys.
*
* @param event The `KeyboardEvent`.
*/
public keyPressed(event: KeyboardEvent): void {
if (this.clearOnEscape && event.key === 'Escape') {
this.clear();
}
this.onkeyup.emit(event);
}
}

View File

@ -35,22 +35,18 @@
</button> </button>
<!-- Search bar --> <!-- Search bar -->
<mat-form-field *ngIf="isSearchBar"> <os-rounded-input
<input [model]="searchFieldInput"
osAutofocus [size]="'small'"
matInput [lazyInput]="true"
(keyup)="applySearch($event, $event.target.value)" (oninput)="searchFieldChange.emit($event)"
placeholder="{{ translate.instant('Search') }}" placeholder="{{ 'Search' | translate }}"
/> ></os-rounded-input>
</mat-form-field>
<button mat-button (click)="toggleSearchBar()">
<mat-icon>{{ isSearchBar ? 'keyboard_arrow_right' : 'search' }}</mat-icon>
</button>
</div> </div>
</div> </div>
<!-- Header for the filter side bar --> <!-- Header for the filter side bar -->
<mat-drawer autoFocus=false #filterMenu mode="push" position="end"> <mat-drawer autoFocus="false" #filterMenu mode="push" position="end">
<div class="custom-table-header filter-menu-head" (click)="this.filterMenu.toggle()"> <div class="custom-table-header filter-menu-head" (click)="this.filterMenu.toggle()">
<span> <span>
<mat-icon>keyboard_arrow_right</mat-icon> <mat-icon>keyboard_arrow_right</mat-icon>

View File

@ -69,3 +69,7 @@ span.right-with-margin {
height: 0px; height: 0px;
width: 0px; width: 0px;
} }
os-rounded-input {
margin: 0 10px;
}

View File

@ -63,6 +63,16 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
@Input() @Input()
public itemsVerboseName: string; public itemsVerboseName: string;
/**
* Custom input for the search-field.
* Used to change the value of the input from outside of this component.
*/
@Input()
public searchFieldInput: string;
/**
* EventEmitter to emit the next search-value.
*/
@Output() @Output()
public searchFieldChange = new EventEmitter<string>(); public searchFieldChange = new EventEmitter<string>();
@ -83,11 +93,6 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
*/ */
private _showFilterSort = true; private _showFilterSort = true;
/**
* The 'opened/active' state of the fulltext filter input field
*/
public isSearchBar = false;
/** /**
* Return the amount of data passing filters. Priorizes the override in {@link filterCount} over * Return the amount of data passing filters. Priorizes the override in {@link filterCount} over
* the information from the filterService * the information from the filterService
@ -183,18 +188,6 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
} }
} }
/**
* Listen to keypresses on the quick-search input
*/
public applySearch(event: KeyboardEvent, value?: string): void {
if (event.key === 'Escape') {
this.searchFieldChange.emit('');
this.isSearchBar = false;
} else {
this.searchFieldChange.emit(value);
}
}
/** /**
* Checks if there is an active SortService present * Checks if there is an active SortService present
*/ */
@ -230,17 +223,4 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
const itemProperty = option.property as string; const itemProperty = option.property as string;
return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1); return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1);
} }
/**
* Open/closes the 'quick search input'. When closing, also removes the filter
* that input applied
*/
public toggleSearchBar(): void {
if (!this.isSearchBar) {
this.isSearchBar = true;
} else {
this.searchFieldChange.emit('');
this.isSearchBar = false;
}
}
} }

View File

@ -94,6 +94,7 @@ import { ListViewTableComponent } from './components/list-view-table/list-view-t
import { AgendaContentObjectFormComponent } from './components/agenda-content-object-form/agenda-content-object-form.component'; import { AgendaContentObjectFormComponent } from './components/agenda-content-object-form/agenda-content-object-form.component';
import { ExtensionFieldComponent } from './components/extension-field/extension-field.component'; import { ExtensionFieldComponent } from './components/extension-field/extension-field.component';
import { AttachmentControlComponent } from './components/attachment-control/attachment-control.component'; import { AttachmentControlComponent } from './components/attachment-control/attachment-control.component';
import { RoundedInputComponent } from './components/rounded-input/rounded-input.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -231,7 +232,8 @@ import { AttachmentControlComponent } from './components/attachment-control/atta
PblNgridTargetEventsModule, PblNgridTargetEventsModule,
ListViewTableComponent, ListViewTableComponent,
AgendaContentObjectFormComponent, AgendaContentObjectFormComponent,
ExtensionFieldComponent ExtensionFieldComponent,
RoundedInputComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -268,7 +270,8 @@ import { AttachmentControlComponent } from './components/attachment-control/atta
ListViewTableComponent, ListViewTableComponent,
AgendaContentObjectFormComponent, AgendaContentObjectFormComponent,
ExtensionFieldComponent, ExtensionFieldComponent,
AttachmentControlComponent AttachmentControlComponent,
RoundedInputComponent
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -21,7 +21,7 @@
[restricted]="restrictedColumns" [restricted]="restrictedColumns"
[hiddenInMobile]="['info']" [hiddenInMobile]="['info']"
[filterProps]="filterProps" [filterProps]="filterProps"
scrollKey="agenda" listStorageKey="agenda"
[(selectedRows)]="selectedRows" [(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >
@ -200,12 +200,21 @@
</button> </button>
<!-- Delete Button --> <!-- Delete Button -->
<button mat-menu-item (click)="removeFromAgenda(item)" *ngIf="item.contentObjectData.collection !== 'topics/topic'"> <button
mat-menu-item
(click)="removeFromAgenda(item)"
*ngIf="item.contentObjectData.collection !== 'topics/topic'"
>
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
<span translate>Remove from agenda</span> <span translate>Remove from agenda</span>
</button> </button>
<button mat-menu-item class="red-warning-text" (click)="deleteTopic(item)" *ngIf="item.contentObjectData.collection === 'topics/topic'"> <button
mat-menu-item
class="red-warning-text"
(click)="deleteTopic(item)"
*ngIf="item.contentObjectData.collection === 'topics/topic'"
>
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>

View File

@ -26,7 +26,7 @@
[columns]="tableColumnDefinition" [columns]="tableColumnDefinition"
[filterProps]="filterProps" [filterProps]="filterProps"
[multiSelect]="isMultiSelect" [multiSelect]="isMultiSelect"
scrollKey="assignments" listStorageKey="assignments"
[(selectedRows)]="selectedRows" [(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >

View File

@ -43,7 +43,7 @@
[showFilterBar]="false" [showFilterBar]="false"
[columns]="tableColumnDefinition" [columns]="tableColumnDefinition"
[multiSelect]="isMultiSelect" [multiSelect]="isMultiSelect"
scrollKey="motionBlock" listStorageKey="motionBlock"
[(selectedRows)]="selectedRows" [(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >

View File

@ -50,7 +50,7 @@
[restricted]="restrictedColumns" [restricted]="restrictedColumns"
[filterProps]="filterProps" [filterProps]="filterProps"
[hiddenInMobile]="['state']" [hiddenInMobile]="['state']"
scrollKey="motion" listStorageKey="motion"
[(selectedRows)]="selectedRows" [(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >

View File

@ -6,7 +6,7 @@
<os-list-view-table <os-list-view-table
[repo]="workflowRepo" [repo]="workflowRepo"
[columns]="tableColumnDefinition" [columns]="tableColumnDefinition"
scrollKey="workflow" listStorageKey="workflow"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >
<!-- Name column --> <!-- Name column -->
@ -40,7 +40,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button type="submit" mat-button color="primary" [disabled]="newWorkflowTitle === ''" [mat-dialog-close]="newWorkflowTitle"> <button
type="submit"
mat-button
color="primary"
[disabled]="newWorkflowTitle === ''"
[mat-dialog-close]="newWorkflowTitle"
>
<span translate>Save</span> <span translate>Save</span>
</button> </button>
<button type="button" mat-button [mat-dialog-close]="null"> <button type="button" mat-button [mat-dialog-close]="null">

View File

@ -22,7 +22,7 @@
[filterProps]="filterProps" [filterProps]="filterProps"
[multiSelect]="isMultiSelect" [multiSelect]="isMultiSelect"
[hiddenInMobile]="['group']" [hiddenInMobile]="['group']"
scrollKey="user" listStorageKey="user"
[(selectedRows)]="selectedRows" [(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >
@ -207,7 +207,11 @@
<mat-icon>vpn_key</mat-icon> <mat-icon>vpn_key</mat-icon>
<span translate>Reset passwords to the default ones</span> <span translate>Reset passwords to the default ones</span>
</button> </button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="generateNewPasswordsPasswordsSelected()"> <button
mat-menu-item
[disabled]="!selectedRows.length"
(click)="generateNewPasswordsPasswordsSelected()"
>
<mat-icon>vpn_key</mat-icon> <mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span> <span translate>Generate new passwords</span>
</button> </button>