TSK-1554: Rework task-filter component with Angular Material

This commit is contained in:
Sofie Hofmann 2021-04-07 09:06:10 +02:00
parent 485a11395e
commit d4bd0b9ef1
11 changed files with 197 additions and 94 deletions

View File

@ -22,7 +22,7 @@ import { ButtonAction } from '../../models/button-action';
import { Pair } from '../../../shared/models/pair'; import { Pair } from '../../../shared/models/pair';
import { WorkbasketQueryFilterParameter } from '../../../shared/models/workbasket-query-filter-parameter'; import { WorkbasketQueryFilterParameter } from '../../../shared/models/workbasket-query-filter-parameter';
import { FilterSelectors } from '../../../shared/store/filter-store/filter.selectors'; import { FilterSelectors } from '../../../shared/store/filter-store/filter.selectors';
import { SetFilter } from '../../../shared/store/filter-store/filter.actions'; import { SetWorkbasketFilter } from '../../../shared/store/filter-store/filter.actions';
export enum Side { export enum Side {
AVAILABLE, AVAILABLE,
@ -283,8 +283,10 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
this.availableDistributionTargetsFilterClone = this.availableDistributionTargets; this.availableDistributionTargetsFilterClone = this.availableDistributionTargets;
this.selectAllRight = true; this.selectAllRight = true;
this.selectAllLeft = true; this.selectAllLeft = true;
this.store.dispatch(new SetFilter(this.selectedDistributionTargetsFilter, 'selectedDistributionTargets')); this.store.dispatch(new SetWorkbasketFilter(this.selectedDistributionTargetsFilter, 'selectedDistributionTargets'));
this.store.dispatch(new SetFilter(this.availableDistributionTargetsFilter, 'availableDistributionTargets')); this.store.dispatch(
new SetWorkbasketFilter(this.availableDistributionTargetsFilter, 'availableDistributionTargets')
);
} }
onClear() { onClear() {

View File

@ -1,46 +1,39 @@
<div class="row"> <div class="task-filter">
<div class="col-xs-2">
<taskana-shared-number-picker [(ngModel)]="filter.priority[0]" <!-- INPUT FIELDS -->
(keyup.enter)="search()" title="priority" <div class="task-filter__input-fields">
id="display-priority-filter"></taskana-shared-number-picker>
<div class="task-filter__row">
<!-- FILTER BY NAME AND OWNER -->
<mat-form-field class="task-filter__input-field--large" style="margin-right: 16px;" matTooltip="Filter Tasks by name">
<mat-label>Filter by name</mat-label>
<input matInput type="text" placeholder="Name" [(ngModel)]="filter['name-like'][0]" (keyup.enter)="search()" (keyup)="updateState()">
</mat-form-field>
<!-- FILTER BY PRIORITY -->
<mat-form-field class="task-filter__input-field--small" matTooltip="Filter Tasks by priority">
<mat-label>Filter by priority</mat-label>
<input matInput type="number" placeholder="Priority" [(ngModel)]="filter.priority[0]" (keyup.enter)="search()" (ngModelChange)="updateState()">
</mat-form-field>
</div>
<div class="task-filter__row">
<mat-form-field class="task-filter__input-field--large" style="margin-right: 16px" matTooltip="Filter Tasks by owner">
<mat-label>Filter by owner</mat-label>
<input matInput type="text" placeholder="Owner" [(ngModel)]="filter['owner-like'][0]" (keyup.enter)="search()" (keyup)="updateState()">
</mat-form-field>
<!-- FILTER BY TASK STATE -->
<mat-form-field class="task-filter__input-field--small" matTooltip="Filter Tasks by status">
<mat-label>Filter by status</mat-label>
<mat-select [value]="filter.state && filter.state[0] ? filter.state[0] : undefined">
<mat-option class="types-selector__options" *ngFor="let state of allStates | mapValues" [value]="state.key" (click)="setStatus(state.key)">
{{ state.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div> </div>
<div class="col-xs-4">
<input type="text" [(ngModel)]="filter['name-like'][0]" (keyup.enter)="search()"
class="form-control" id="display-name-filter"
placeholder="Filter name">
</div>
<div class="col-xs-4">
<input type="text" [(ngModel)]="filter['owner-like'][0]" (keyup.enter)="search()"
class="form-control" id="display-owner-filter"
placeholder="Filter owner">
</div>
<button (click)="clear(); search()" class="btn btn-default pull-right margin-right" type="button"
data-toggle="tooltip"
title="Clear">
<span class="material-icons md-20 blue">clear</span>
</button>
</div>
<div class="row">
<div class="dropdown col-xs-2 col-xs-offset-2">
<button class="btn btn-default" type="button" data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
title="State: {{filter.state && filter.state[0] ? filter.state[0] : 'All'}}">
<span>{{filter.state ? filter.state[0] : 'All'}}</span>
</button>
<ul class="dropdown-menu dropdown-menu-users" role="menu">
<li>
<a *ngFor="let state of allStates | mapValues" type="button"
(click)="selectState(state.key); search()"
data-toggle="tooltip" [title]="state.value">
<label class="blue">{{state.value}}</label>
</a>
</li>
</ul>
</div>
<button (click)="search()" type="button" class="btn btn-default pull-right margin-right"
data-toggle="tooltip"
title="Search">
<span class="material-icons md-20 blue">search</span>
</button>
</div> </div>

View File

@ -1,15 +1,34 @@
.dropdown-menu-users { @import 'src/theme/_colors.scss';
& > li {
margin-bottom: 5px; .task-filter {
display: flex;
flex-direction: column;
&__button--primary {
background: $aquamarine;
color: white;
position: relative;
top: 30px;
} }
margin-left: 15px; &__button--secondary {
} position: relative;
top: 24px;
}
button.btn.btn-default.pull-right.margin-right { .task-filter__row {
margin-top: 1px; display: flex;
} flex-direction: row;
}
&__input-field--large {
width: 320px;
}
&__input-field--small {
flex-grow: 1;
min-width: 140px;
}
.blue {
color: #2e9eca;
} }

View File

@ -1,29 +1,39 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ALL_STATES, TaskState } from '../../models/task-state'; import { ALL_STATES, TaskState } from '../../models/task-state';
import { TaskQueryFilterParameter } from '../../models/task-query-filter-parameter'; import { TaskQueryFilterParameter } from '../../models/task-query-filter-parameter';
import { Actions, ofActionCompleted, Store } from '@ngxs/store';
import { ClearTaskFilter, SetTaskFilter } from '../../store/filter-store/filter.actions';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({ @Component({
selector: 'taskana-shared-task-filter', selector: 'taskana-shared-task-filter',
templateUrl: './task-filter.component.html', templateUrl: './task-filter.component.html',
styleUrls: ['./task-filter.component.scss'] styleUrls: ['./task-filter.component.scss']
}) })
export class TaskFilterComponent implements OnInit { export class TaskFilterComponent implements OnInit, OnDestroy {
filter: TaskQueryFilterParameter; filter: TaskQueryFilterParameter;
destroy$ = new Subject<void>();
@Output() performFilter = new EventEmitter<TaskQueryFilterParameter>();
allStates: Map<TaskState, string> = ALL_STATES; allStates: Map<TaskState, string> = ALL_STATES;
ngOnInit(): void { constructor(private store: Store, private ngxsActions$: Actions) {}
ngOnInit() {
this.clear(); this.clear();
this.ngxsActions$.pipe(ofActionCompleted(ClearTaskFilter), takeUntil(this.destroy$)).subscribe(() => this.clear());
} }
selectState(state: TaskState) { setStatus(state: TaskState) {
this.filter.state = state ? [state] : []; this.filter.state = state ? [state] : [];
this.updateState();
} }
search() { // TODO: filter tasks when pressing 'enter'
this.performFilter.emit(this.filter); search() {}
updateState() {
this.store.dispatch(new SetTaskFilter(this.filter));
} }
clear() { clear() {
@ -33,4 +43,9 @@ export class TaskFilterComponent implements OnInit {
'owner-like': [] 'owner-like': []
}; };
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
} }

View File

@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ALL_TYPES, WorkbasketType } from '../../models/workbasket-type'; import { ALL_TYPES, WorkbasketType } from '../../models/workbasket-type';
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter'; import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { Select, Store } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { ClearFilter, SetFilter } from '../../store/filter-store/filter.actions'; import { ClearWorkbasketFilter, SetWorkbasketFilter } from '../../store/filter-store/filter.actions';
import { FilterSelectors } from '../../store/filter-store/filter.selectors'; import { FilterSelectors } from '../../store/filter-store/filter.selectors';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -60,7 +60,7 @@ export class WorkbasketFilterComponent implements OnInit, OnDestroy {
} }
clear() { clear() {
this.store.dispatch(new ClearFilter(this.component)); this.store.dispatch(new ClearWorkbasketFilter(this.component));
} }
selectType(type: WorkbasketType) { selectType(type: WorkbasketType) {
@ -68,7 +68,7 @@ export class WorkbasketFilterComponent implements OnInit, OnDestroy {
} }
search() { search() {
this.store.dispatch(new SetFilter(this.filter, this.component)); this.store.dispatch(new SetWorkbasketFilter(this.filter, this.component));
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -1,11 +1,23 @@
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter'; import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { TaskQueryFilterParameter } from '../../models/task-query-filter-parameter';
export class SetFilter { // Workbasket Filter
static readonly type = '[Workbasket filter] Set filter parameter'; export class SetWorkbasketFilter {
static readonly type = '[Workbasket filter] Set workbasket filter parameter';
constructor(public parameters: WorkbasketQueryFilterParameter, public component: string) {} constructor(public parameters: WorkbasketQueryFilterParameter, public component: string) {}
} }
export class ClearFilter { export class ClearWorkbasketFilter {
static readonly type = '[Workbasket filter] Clear filter parameter'; static readonly type = '[Workbasket filter] Clear workbasket filter parameter';
constructor(public component: string) {} constructor(public component: string) {}
} }
// Task Filter
export class SetTaskFilter {
static readonly type = '[Task filter] Set task filter parameter';
constructor(public parameters: TaskQueryFilterParameter) {}
}
export class ClearTaskFilter {
static readonly type = '[Task filter] Clear task filter parameter';
}

View File

@ -1,6 +1,7 @@
import { FilterState, FilterStateModel } from './filter.state'; import { FilterState, FilterStateModel } from './filter.state';
import { Selector } from '@ngxs/store'; import { Selector } from '@ngxs/store';
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter'; import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { TaskQueryFilterParameter } from '../../models/task-query-filter-parameter';
export class FilterSelectors { export class FilterSelectors {
@Selector([FilterState]) @Selector([FilterState])
@ -17,4 +18,9 @@ export class FilterSelectors {
static getWorkbasketListFilter(state: FilterStateModel): WorkbasketQueryFilterParameter { static getWorkbasketListFilter(state: FilterStateModel): WorkbasketQueryFilterParameter {
return state.workbasketList; return state.workbasketList;
} }
@Selector([FilterState])
static getTaskFilter(state: FilterStateModel): TaskQueryFilterParameter {
return state.tasks;
}
} }

View File

@ -1,9 +1,10 @@
import { Action, NgxsOnInit, State, StateContext } from '@ngxs/store'; import { Action, NgxsOnInit, State, StateContext } from '@ngxs/store';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter'; import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { ClearFilter, SetFilter } from './filter.actions'; import { ClearTaskFilter, ClearWorkbasketFilter, SetTaskFilter, SetWorkbasketFilter } from './filter.actions';
import { TaskQueryFilterParameter } from '../../models/task-query-filter-parameter';
const emptyFilter: WorkbasketQueryFilterParameter = { const emptyWorkbasketFilter: WorkbasketQueryFilterParameter = {
'description-like': [], 'description-like': [],
'key-like': [], 'key-like': [],
'name-like': [], 'name-like': [],
@ -11,10 +12,20 @@ const emptyFilter: WorkbasketQueryFilterParameter = {
type: [] type: []
}; };
const emptyTaskFilter: TaskQueryFilterParameter = {
'name-like': [],
'owner-like': [],
state: [],
priority: [],
'por.value': [],
'wildcard-search-fields': [],
'wildcard-search-value': []
};
@State<FilterStateModel>({ name: 'FilterState' }) @State<FilterStateModel>({ name: 'FilterState' })
export class FilterState implements NgxsOnInit { export class FilterState implements NgxsOnInit {
@Action(SetFilter) @Action(SetWorkbasketFilter)
setAvailableDistributionTargetsFilter(ctx: StateContext<FilterStateModel>, action: SetFilter): Observable<null> { setWorkbasketFilter(ctx: StateContext<FilterStateModel>, action: SetWorkbasketFilter): Observable<null> {
const currentState = ctx.getState()[action.component]; const currentState = ctx.getState()[action.component];
const param = action.parameters; const param = action.parameters;
const filter: WorkbasketQueryFilterParameter = { const filter: WorkbasketQueryFilterParameter = {
@ -33,22 +44,66 @@ export class FilterState implements NgxsOnInit {
return of(null); return of(null);
} }
@Action(ClearFilter) @Action(ClearWorkbasketFilter)
clearFilter(ctx: StateContext<FilterStateModel>, action: ClearFilter): Observable<null> { clearWorkbasketFilter(ctx: StateContext<FilterStateModel>, action: ClearWorkbasketFilter): Observable<null> {
ctx.setState({ ctx.setState({
...ctx.getState(), ...ctx.getState(),
[action.component]: { ...emptyFilter } [action.component]: { ...emptyWorkbasketFilter }
}); });
return of(null); return of(null);
} }
@Action(SetTaskFilter)
setTaskFilter(ctx: StateContext<FilterStateModel>, action: SetTaskFilter): Observable<null> {
const param = action.parameters;
let filter = { ...ctx.getState().tasks };
Object.keys(param).forEach((key) => {
filter[key] = [...param[key]];
});
const isWildcardSearch = filter['wildcard-search-value'].length !== 0 && filter['wildcard-search-value'] !== [''];
filter['wildcard-search-fields'] = isWildcardSearch ? this.initWildcardFields() : [];
// Delete wildcard search field 'NAME' if 'name-like' exists
if (filter['name-like'].length > 0 && filter['name-like'][0] !== '') {
filter['wildcard-search-fields'].shift();
}
ctx.setState({
...ctx.getState(),
tasks: filter
});
return of(null);
}
@Action(ClearTaskFilter)
clearTaskFilter(ctx: StateContext<FilterStateModel>): Observable<null> {
ctx.setState({
...ctx.getState(),
tasks: { ...emptyTaskFilter }
});
return of(null);
}
initWildcardFields() {
let wildcardSearchFields = ['NAME', 'DESCRIPTION'];
[...Array(16).keys()].map((number) => {
wildcardSearchFields.push(`CUSTOM_${number + 1}`);
});
return wildcardSearchFields;
}
ngxsOnInit(ctx: StateContext<FilterStateModel>): void { ngxsOnInit(ctx: StateContext<FilterStateModel>): void {
ctx.setState({ ctx.setState({
...ctx.getState(), ...ctx.getState(),
availableDistributionTargets: emptyFilter, availableDistributionTargets: emptyWorkbasketFilter,
selectedDistributionTargets: emptyFilter, selectedDistributionTargets: emptyWorkbasketFilter,
workbasketList: emptyFilter workbasketList: emptyWorkbasketFilter,
tasks: emptyTaskFilter
}); });
} }
} }
@ -57,4 +112,5 @@ export interface FilterStateModel {
availableDistributionTargets: WorkbasketQueryFilterParameter; availableDistributionTargets: WorkbasketQueryFilterParameter;
selectedDistributionTargets: WorkbasketQueryFilterParameter; selectedDistributionTargets: WorkbasketQueryFilterParameter;
workbasketList: WorkbasketQueryFilterParameter; workbasketList: WorkbasketQueryFilterParameter;
tasks: TaskQueryFilterParameter;
} }

View File

@ -37,7 +37,7 @@ import { RequestInProgressService } from '../../services/request-in-progress/req
import { WorkbasketType } from '../../models/workbasket-type'; import { WorkbasketType } from '../../models/workbasket-type';
import { TaskanaDate } from '../../util/taskana.date'; import { TaskanaDate } from '../../util/taskana.date';
import { DomainService } from '../../services/domain/domain.service'; import { DomainService } from '../../services/domain/domain.service';
import { ClearFilter } from '../filter-store/filter.actions'; import { ClearWorkbasketFilter } from '../filter-store/filter.actions';
class InitializeStore { class InitializeStore {
static readonly type = '[Workbasket] Initializing state'; static readonly type = '[Workbasket] Initializing state';
@ -136,8 +136,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
.replace(/(workbaskets).*/g, `workbaskets/(detail:${action.workbasketId})?tab=${selectedComponent}`) .replace(/(workbaskets).*/g, `workbaskets/(detail:${action.workbasketId})?tab=${selectedComponent}`)
); );
ctx.dispatch(new ClearFilter('selectedDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearFilter('availableDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
}) })
); );
} }
@ -224,8 +224,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
badgeMessage: `Copying workbasket: ${workbasket.key}` badgeMessage: `Copying workbasket: ${workbasket.key}`
}); });
ctx.dispatch(new ClearFilter('selectedDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearFilter('availableDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
return of(null); return of(null);
} }
@ -261,8 +261,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
workbasketDistributionTargets: distributionTargets workbasketDistributionTargets: distributionTargets
}); });
ctx.dispatch(new ClearFilter('selectedDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearFilter('availableDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
return of(null); return of(null);
}) })
@ -433,8 +433,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
selectedWorkbasket, selectedWorkbasket,
action: ACTION.READ action: ACTION.READ
}); });
ctx.dispatch(new ClearFilter('selectedDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearFilter('availableDistributionTargets')); ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
}); });
} }
this.requestInProgressService.setRequestInProgress(false); this.requestInProgressService.setRequestInProgress(false);

View File

@ -35,13 +35,13 @@
<!-- SEARCH BY TYPE --> <!-- SEARCH BY TYPE -->
<mat-form-field style="padding-right: 8px"> <mat-form-field style="padding-right: 8px">
<mat-label>Type</mat-label> <mat-label>Filter by type</mat-label>
<input matInput type="text" placeholder="Type" [(ngModel)]="resultType" (keyup.enter)="searchBasket()"> <input matInput type="text" placeholder="Type" [(ngModel)]="resultType" (keyup.enter)="searchBasket()">
</mat-form-field> </mat-form-field>
<!-- SEARCH BY VALUE--> <!-- SEARCH BY VALUE-->
<mat-form-field style="padding-right: 8px"> <mat-form-field style="padding-right: 8px">
<mat-label>Value</mat-label> <mat-label>Filter by value</mat-label>
<input matInput type="text" placeholder="Value" [(ngModel)]="resultValue" (keyup.enter)="searchBasket()"> <input matInput type="text" placeholder="Value" [(ngModel)]="resultValue" (keyup.enter)="searchBasket()">
</mat-form-field> </mat-form-field>

View File

@ -33,7 +33,7 @@
<!-- EMPTY TASK-LIST --> <!-- EMPTY TASK-LIST -->
<ng-template #empty_list> <ng-template #empty_list>
<div style="margin-top: 60px" class="container-no-items center-block"> <div style="margin-top: 60px" class="container-no-items center-block">
<h3 class="grey">Select a workbasket</h3> <h3 class="grey">Select a Workbasket</h3>
<svg-icon class="img-responsive empty-icon workbasket-icon" src="./assets/icons/wb-empty.svg"></svg-icon> <svg-icon class="img-responsive empty-icon workbasket-icon" src="./assets/icons/wb-empty.svg"></svg-icon>
</div> </div>
</ng-template> </ng-template>