#683 Add Chips to Filter View

This commit is contained in:
Sebastian
2020-06-26 10:38:58 +02:00
parent a012599f27
commit 0bda33a327
18 changed files with 285 additions and 19 deletions

View File

@@ -1,4 +1,4 @@
// start:ng42.barrel
export * from './shell-search-device.model';
export * from './shelf-primary-filter-options';
// end:ng42.barrel

View File

@@ -0,0 +1,6 @@
export interface ShelfPrimaryFilterOptions {
allBranches: boolean;
customerName: boolean;
author: boolean;
title: boolean;
}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './shelf-primary-filters.component';
// end:ng42.barrel

View File

@@ -0,0 +1,34 @@
<div class="container" *ngIf="primaryFilters$ | async as primaryFilters">
<button
class="isa-chip"
id="allBranches"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['allBranches']"
>
Alle Filialen
</button>
<button
class="isa-chip"
id="customerName"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['customerName']"
>
Kundenname
</button>
<button
class="isa-chip"
id="author"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['author']"
>
Autor
</button>
<button
class="isa-chip"
id="title"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['title']"
>
Titel
</button>
</div>

View File

@@ -0,0 +1,7 @@
.container {
display: grid;
align-items: center;
justify-content: center;
grid-gap: 30px;
grid-template-columns: repeat(4, auto);
}

View File

@@ -0,0 +1,51 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
Renderer2,
} from '@angular/core';
import { ShelfPrimaryFilterOptions } from '../../../defs';
import { Observable } from 'rxjs';
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
import { map, take } from 'rxjs/operators';
@Component({
selector: 'app-shelf-primary-filters',
templateUrl: 'shelf-primary-filters.component.html',
styleUrls: ['./shelf-primary-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShelfPrimaryFiltersComponent implements OnInit {
primaryFilters$: Observable<ShelfPrimaryFilterOptions>;
constructor(private searchStateFacade: SearchStateFacade) {}
ngOnInit() {
this.initFilters();
}
handleClick(target: HTMLButtonElement) {
const identifier = target.id;
if (!identifier) {
return;
}
this.handleUpdate({ identifier });
}
private handleUpdate(params: { identifier: string }) {
this.primaryFilters$.pipe(take(1)).subscribe((currentFilters) => {
const updatedValue = !currentFilters[params.identifier];
this.updateSelectedFilters({ [params.identifier]: updatedValue });
});
}
private updateSelectedFilters(changes: Partial<ShelfPrimaryFilterOptions>) {
this.searchStateFacade.setPrimaryFilters(changes);
}
private initFilters() {
this.primaryFilters$ = this.searchStateFacade.getPrimaryFilters();
}
}

View File

@@ -14,5 +14,9 @@
></lib-icon>
</button>
<h2 class="isa-filter-title">Filter</h2>
<div>
<app-shelf-primary-filters></app-shelf-primary-filters>
</div>
</div>
</div>

View File

@@ -3,11 +3,12 @@ import { CommonModule } from '@angular/common';
import { ShelfFilterComponent } from './shelf-filter.component';
import { SharedModule } from 'apps/sales/src/app/shared/shared.module';
import { IconModule } from '@libs/ui';
import { ShelfPrimaryFiltersComponent } from './primary-filters';
@NgModule({
imports: [CommonModule, SharedModule, IconModule],
exports: [ShelfFilterComponent],
declarations: [ShelfFilterComponent],
exports: [ShelfFilterComponent, ShelfPrimaryFiltersComponent],
declarations: [ShelfFilterComponent, ShelfPrimaryFiltersComponent],
entryComponents: [ShelfFilterComponent],
})
export class ShelfFilterModule {}

View File

@@ -1,5 +1,10 @@
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
export interface SearchProcess {
id: number; // Prozess ID;
input?: string;
filters?: { [key: string]: string[] };
filters?: {
selectedFilters?: { [key: string]: string[] };
primaryFilters?: ShelfPrimaryFilterOptions;
};
}

View File

@@ -1,4 +1,5 @@
import { createAction, props } from '@ngrx/store';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
const prefix = '[CUSTOMER] [SHELF] [SEARCH]';
@@ -17,12 +18,17 @@ export const setInput = createAction(
props<{ id: number; input: string }>()
);
export const setFilters = createAction(
`${prefix} Set Filters`,
export const setSelectedFilters = createAction(
`${prefix} Set Selected Filters`,
props<{ id: number; filters: { [key: string]: string[] } }>()
);
export const clearFilters = createAction(
`${prefix} Clear Filters`,
export const clearSelectedFilters = createAction(
`${prefix} Clear Selected Filters`,
props<{ id: number }>()
);
export const setPrimaryFilters = createAction(
`${prefix} Set Primary Filters`,
props<{ id: number; filters: ShelfPrimaryFilterOptions }>()
);

View File

@@ -5,9 +5,13 @@ import * as actions from './search.actions';
import { SharedSelectors } from 'apps/sales/src/app/core/store/selectors/shared.selectors';
import { map, first, take, switchMap, filter } from 'rxjs/operators';
import { from, Observable } from 'rxjs';
import { selectSearchProcessById } from './search.selectors';
import {
selectSearchProcessById,
selectProcessPrimaryFiltersById,
} from './search.selectors';
import { SearchProcess } from './defs';
import { isNullOrUndefined } from 'util';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
@Injectable({ providedIn: 'root' })
export class SearchStateFacade {
@@ -35,8 +39,11 @@ export class SearchStateFacade {
get currentSearchProcessFilters$(): Observable<{ [key: string]: string[] }> {
return this.currentSearchProcess$.pipe(
filter((process) => !isNullOrUndefined(process)),
map((process) => process.filters)
filter(
(process) =>
!isNullOrUndefined(process) && !isNullOrUndefined(process.filters)
),
map((process) => process.filters.selectedFilters)
);
}
@@ -50,13 +57,54 @@ export class SearchStateFacade {
this.store.dispatch(actions.setInput({ input, id: processId }));
}
async setFilters(filters: { [key: string]: string[] }, id?: number) {
async setSelectedFilters(filters: { [key: string]: string[] }, id?: number) {
if (id) {
return this.store.dispatch(actions.setFilters({ filters, id }));
return this.store.dispatch(actions.setSelectedFilters({ filters, id }));
}
const processId = await this.getProcessId();
this.store.dispatch(actions.setFilters({ filters, id: processId }));
this.store.dispatch(actions.setSelectedFilters({ filters, id: processId }));
}
async setPrimaryFilters(
filters: Partial<ShelfPrimaryFilterOptions>,
id?: number
) {
let updatedFilters: ShelfPrimaryFilterOptions;
let processId = id;
if (!id) {
processId = await this.getProcessId();
}
const currentPrimaryFilters = await this.getCurrentPrimaryFilters(processId)
.pipe(first())
.toPromise();
updatedFilters = {
...currentPrimaryFilters,
...filters,
} as ShelfPrimaryFilterOptions;
return this.store.dispatch(
actions.setPrimaryFilters({ filters: updatedFilters, id: processId })
);
}
getPrimaryFilters(id?: number): Observable<ShelfPrimaryFilterOptions> {
if (id) {
return this.getCurrentPrimaryFilters(id);
}
return from(this.getProcessId()).pipe(
switchMap((processId) => this.getCurrentPrimaryFilters(processId))
);
}
private getCurrentPrimaryFilters(
processId: number
): Observable<ShelfPrimaryFilterOptions> {
return this.store.select(selectProcessPrimaryFiltersById, processId);
}
}

View File

@@ -3,13 +3,14 @@ import {
INITIAL_SEARCH_STATE,
SearchState,
searchStateAdapter,
INITIAL_FILTERS,
} from './search.state';
import * as actions from './search.actions';
const _searchReducer = createReducer(
INITIAL_SEARCH_STATE,
on(actions.addSearchProcess, (s, a) =>
searchStateAdapter.addOne({ id: a.id }, s)
searchStateAdapter.addOne({ id: a.id, filters: INITIAL_FILTERS }, s)
),
on(actions.removeSearchProcess, (s, a) =>
searchStateAdapter.removeOne(a.id, s)
@@ -17,22 +18,30 @@ const _searchReducer = createReducer(
on(actions.setInput, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { input: a.input } }, s)
),
on(actions.setFilters, (s, a) =>
on(actions.setSelectedFilters, (s, a) =>
searchStateAdapter.updateOne(
{ id: a.id, changes: { filters: a.filters } },
{ id: a.id, changes: { filters: { selectedFilters: a.filters } } },
s
)
),
on(actions.clearFilters, (s, a) =>
on(actions.clearSelectedFilters, (s, a) =>
searchStateAdapter.updateOne(
{
id: a.id,
changes: {
filters: {},
filters: {
selectedFilters: {},
},
},
},
s
)
),
on(actions.setPrimaryFilters, (s, a) =>
searchStateAdapter.updateOne(
{ id: a.id, changes: { filters: { primaryFilters: a.filters } } },
s
)
)
);

View File

@@ -31,3 +31,25 @@ export const selectSearchProcessFiltersById = createSelector(
}
}
);
export const selectProcessSelectedFiltersById = createSelector(
selectAllSearchProcesses,
(s: SearchProcess[], processId: number) => {
const searchProcess = s.find((p) => p.id === processId);
if (searchProcess && searchProcess.filters) {
return searchProcess.filters.selectedFilters;
}
}
);
export const selectProcessPrimaryFiltersById = createSelector(
selectAllSearchProcesses,
(s: SearchProcess[], processId: number) => {
const searchProcess = s.find((p) => p.id === processId);
if (searchProcess && searchProcess.filters) {
return searchProcess.filters.primaryFilters;
}
}
);

View File

@@ -1,5 +1,6 @@
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { SearchProcess } from './defs';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
export interface SearchState extends EntityState<SearchProcess> {}
@@ -8,3 +9,18 @@ export const searchStateAdapter = createEntityAdapter<SearchProcess>();
export const INITIAL_SEARCH_STATE: SearchState = {
...searchStateAdapter.getInitialState(),
};
export const INITIAL_PRIMARY_FILTERS: ShelfPrimaryFilterOptions = {
allBranches: false,
customerName: false,
author: false,
title: false,
};
export const INITIAL_FILTERS: {
selectedFilters?: { [key: string]: string[] };
primaryFilters?: ShelfPrimaryFilterOptions;
} = {
selectedFilters: {},
primaryFilters: INITIAL_PRIMARY_FILTERS,
};

View File

@@ -7,6 +7,7 @@
/* MODULES */
@import 'modules/button';
@import 'modules/card';
@import 'modules/chip';
@import 'modules/content';
@import 'modules/forms';
@import 'modules/modal';

View File

@@ -14,6 +14,7 @@ $text-black: #000000;
$isa-red: #f70400;
$isa-white: #ffffff;
$isa-lightgray: #e2e2e2;
$isa-customer: #557596;
$isa-customer-active: #59647a;
$isa-customer-active: #1f466c;
@@ -41,6 +42,11 @@ $big-desktop: 1800px;
$content-background-color: #fff;
$content-border-radius: 5px;
/** CHIP */
$chip-border-radius: 27px;
$chip-padding: 0 20px 0 21px;
$chip-height: 53px;
/* HEADLINE */
$headline-font-size-l: 26px;
$headline-font-size-m: 22px;

View File

@@ -0,0 +1,45 @@
.isa-chip {
background: $isa-white;
color: $isa-customer;
border: 2px solid transparent;
border-radius: $chip-border-radius;
font-family: $font-family;
font-weight: $font-weight-emphasis;
font-size: $font-size;
line-height: $button-line-height-l;
padding: $chip-padding;
width: fit-content;
height: $chip-height;
display: flex;
align-items: center;
justify-content: center;
&.selected {
position: relative;
font-weight: $font-weight-bold;
color: $isa-customer;
border: 2px solid $isa-customer;
&::before {
content: '';
background-image: url(/assets/images/Check.svg);
position: relative;
left: 0px;
top: 4px;
width: 21px;
height: 21px;
background-size: 16px;
background-repeat: no-repeat;
transition: all 2s ease;
}
}
}
// Reset Default Button Styles
button.isa-chip {
border: none;
outline: none;
appearance: none;
-webkit-appearance: none;
}

View File

@@ -13,6 +13,7 @@
.isa-filter-title {
text-align: center;
margin-top: 25px;
margin-bottom: 32px;
}
.isa-btn-close {