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

View File

@ -64,7 +64,7 @@ export interface ColumnRestriction {
* [hiddenInMobile]="['state']"
* [allowProjector]="false"
* [multiSelect]="isMultiSelect"
* scrollKey="motion"
* listStorageKey="motion"
* [(selectedRows)]="selectedRows"
* (dataSourceChange)="onDataSourceChange($event)"
* >
@ -157,7 +157,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
* Key to restore scroll position after navigating
*/
@Input()
public scrollKey: string;
public listStorageKey: string;
/**
* Wether or not to show the filter bar
@ -196,6 +196,12 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
*/
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
*/
@ -365,8 +371,9 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
.build();
// restore scroll position
if (this.scrollKey) {
this.scrollToPreviousPosition(this.scrollKey);
if (this.listStorageKey) {
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
*/
public searchFilter(filterValue: string): void {
if (this.listStorageKey) {
this.saveSearchQuery(this.listStorageKey, filterValue);
}
this.inputValue = filterValue;
if (this.initialLoading) {
this.initialLoading = false;
} else {
this.dataSource.syncFilter();
}
}
/**
* Loads the scroll-index from the storage
@ -424,6 +438,25 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
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
*

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>
<!-- Search bar -->
<mat-form-field *ngIf="isSearchBar">
<input
osAutofocus
matInput
(keyup)="applySearch($event, $event.target.value)"
placeholder="{{ translate.instant('Search') }}"
/>
</mat-form-field>
<button mat-button (click)="toggleSearchBar()">
<mat-icon>{{ isSearchBar ? 'keyboard_arrow_right' : 'search' }}</mat-icon>
</button>
<os-rounded-input
[model]="searchFieldInput"
[size]="'small'"
[lazyInput]="true"
(oninput)="searchFieldChange.emit($event)"
placeholder="{{ 'Search' | translate }}"
></os-rounded-input>
</div>
</div>
<!-- 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()">
<span>
<mat-icon>keyboard_arrow_right</mat-icon>

View File

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

View File

@ -63,6 +63,16 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
@Input()
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()
public searchFieldChange = new EventEmitter<string>();
@ -83,11 +93,6 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
*/
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
* 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
*/
@ -230,17 +223,4 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
const itemProperty = option.property as string;
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 { ExtensionFieldComponent } from './components/extension-field/extension-field.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.
@ -231,7 +232,8 @@ import { AttachmentControlComponent } from './components/attachment-control/atta
PblNgridTargetEventsModule,
ListViewTableComponent,
AgendaContentObjectFormComponent,
ExtensionFieldComponent
ExtensionFieldComponent,
RoundedInputComponent
],
declarations: [
PermsDirective,
@ -268,7 +270,8 @@ import { AttachmentControlComponent } from './components/attachment-control/atta
ListViewTableComponent,
AgendaContentObjectFormComponent,
ExtensionFieldComponent,
AttachmentControlComponent
AttachmentControlComponent,
RoundedInputComponent
],
providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -21,7 +21,7 @@
[restricted]="restrictedColumns"
[hiddenInMobile]="['info']"
[filterProps]="filterProps"
scrollKey="agenda"
listStorageKey="agenda"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
>
@ -200,12 +200,21 @@
</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>
<span translate>Remove from agenda</span>
</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>
<span translate>Delete</span>
</button>

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
<os-list-view-table
[repo]="workflowRepo"
[columns]="tableColumnDefinition"
scrollKey="workflow"
listStorageKey="workflow"
(dataSourceChange)="onDataSourceChange($event)"
>
<!-- Name column -->
@ -36,11 +36,17 @@
<div mat-dialog-content>
<p translate>Please enter a name for the new workflow:</p>
<mat-form-field>
<input matInput osAutofocus [(ngModel)]="newWorkflowTitle" required/>
<input matInput osAutofocus [(ngModel)]="newWorkflowTitle" required />
</mat-form-field>
</div>
<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>
</button>
<button type="button" mat-button [mat-dialog-close]="null">

View File

@ -22,7 +22,7 @@
[filterProps]="filterProps"
[multiSelect]="isMultiSelect"
[hiddenInMobile]="['group']"
scrollKey="user"
listStorageKey="user"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
>
@ -207,7 +207,11 @@
<mat-icon>vpn_key</mat-icon>
<span translate>Reset passwords to the default ones</span>
</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>
<span translate>Generate new passwords</span>
</button>