Merge pull request #4882 from GabrielInTheWorld/savingSearchInBar
Saves the value of the local search in 'list-view-table'
This commit is contained in:
commit
56db9b0723
@ -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"
|
||||
|
@ -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,8 +416,15 @@ 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;
|
||||
this.dataSource.syncFilter();
|
||||
if (this.initialLoading) {
|
||||
this.initialLoading = false;
|
||||
} else {
|
||||
this.dataSource.syncFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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
|
||||
*
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -69,3 +69,7 @@ span.right-with-margin {
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
os-rounded-input {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 },
|
||||
|
@ -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>
|
||||
|
@ -26,7 +26,7 @@
|
||||
[columns]="tableColumnDefinition"
|
||||
[filterProps]="filterProps"
|
||||
[multiSelect]="isMultiSelect"
|
||||
scrollKey="assignments"
|
||||
listStorageKey="assignments"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($event)"
|
||||
>
|
||||
|
@ -43,7 +43,7 @@
|
||||
[showFilterBar]="false"
|
||||
[columns]="tableColumnDefinition"
|
||||
[multiSelect]="isMultiSelect"
|
||||
scrollKey="motionBlock"
|
||||
listStorageKey="motionBlock"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($event)"
|
||||
>
|
||||
|
@ -50,7 +50,7 @@
|
||||
[restricted]="restrictedColumns"
|
||||
[filterProps]="filterProps"
|
||||
[hiddenInMobile]="['state']"
|
||||
scrollKey="motion"
|
||||
listStorageKey="motion"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($event)"
|
||||
>
|
||||
|
@ -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">
|
||||
|
@ -22,7 +22,7 @@
|
||||
[filterProps]="filterProps"
|
||||
[multiSelect]="isMultiSelect"
|
||||
[hiddenInMobile]="['group']"
|
||||
scrollKey="user"
|
||||
listStorageKey="user"
|
||||
[(selectedRows)]="selectedRows"
|
||||
(dataSourceChange)="onDataSourceChange($event)"
|
||||
>
|
||||
@ -204,10 +204,14 @@
|
||||
</button>
|
||||
|
||||
<button mat-menu-item [disabled]="!selectedRows.length" (click)="resetPasswordsToDefaultSelected()">
|
||||
<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()">
|
||||
<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()"
|
||||
>
|
||||
<mat-icon>vpn_key</mat-icon>
|
||||
<span translate>Generate new passwords</span>
|
||||
</button>
|
||||
|
Loading…
Reference in New Issue
Block a user