mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
#932 - Kundenfilter
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StringDictionary } from '@cmf/core';
|
||||
import { AutocompleteDTO, CustomerInfoDTO, CustomerService, InputDTO } from '@swagger/crm';
|
||||
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -7,22 +8,23 @@ import { Observable } from 'rxjs';
|
||||
export class CrmCustomerService {
|
||||
constructor(private customerService: CustomerService) {}
|
||||
|
||||
complete(queryString: string): Observable<Result<AutocompleteDTO[]>> {
|
||||
complete(queryString: string, filter?: StringDictionary<string>): Observable<Result<AutocompleteDTO[]>> {
|
||||
return this.customerService.CustomerCustomerAutocomplete({
|
||||
input: queryString,
|
||||
filter: {},
|
||||
filter,
|
||||
take: 5,
|
||||
});
|
||||
}
|
||||
|
||||
getCustomers(
|
||||
queryString: string,
|
||||
options: { take?: number; skip?: number } = { take: 20, skip: 0 }
|
||||
options: { take?: number; skip?: number; filter?: StringDictionary<string> } = { take: 20, skip: 0 }
|
||||
): Observable<PagedResult<CustomerInfoDTO>> {
|
||||
return this.customerService.CustomerListCustomers({
|
||||
input: { qs: queryString },
|
||||
take: options.take,
|
||||
skip: options.skip,
|
||||
filter: options.filter,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<button class="filter" [class.active]="true" (click)="filterActive = true">
|
||||
<button class="filter" [class.active]="true" (click)="showFilters = true">
|
||||
<ui-icon size="22px" icon="filter_alit"></ui-icon>
|
||||
<span class="label">Filter</span>
|
||||
</button>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div class="filter-overlay" [class.active]="filterActive">
|
||||
<div class="filter-overlay" [class.active]="showFilters">
|
||||
<div class="filter-content">
|
||||
<div class="filter-close-right">
|
||||
<button class="filter-close" (click)="filterActive = false">
|
||||
<button class="filter-close" (click)="showFilters = false">
|
||||
<ui-icon size="20px" icon="close"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="filter-header">
|
||||
Filter
|
||||
</h2>
|
||||
<page-customer-search-filter></page-customer-search-filter>
|
||||
<page-customer-search-filter *ngIf="showFilters"> </page-customer-search-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,6 @@ import { CustomerSearch } from './customer-search.service';
|
||||
],
|
||||
})
|
||||
export class CustomerSearchComponent extends CustomerSearch implements OnInit {
|
||||
filterActive = false;
|
||||
|
||||
constructor(
|
||||
protected customerSearch: CrmCustomerService,
|
||||
zone: NgZone,
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { PagedResult } from '@domain/defs';
|
||||
import { AutocompleteDTO, CustomerInfoDTO } from '@swagger/crm';
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { catchError, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { CustomerSearchType, QueryFilter, ResultState } from './defs';
|
||||
import { Filter, UiFilterMappingService } from '@ui/filter';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { StringDictionary } from '@cmf/core';
|
||||
|
||||
@Injectable()
|
||||
export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
@@ -32,8 +24,6 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
return this.queryFilter$.value;
|
||||
}
|
||||
|
||||
availableFilters$: Observable<Filter[]>;
|
||||
|
||||
autocompleteResult$: Observable<AutocompleteDTO[]>;
|
||||
|
||||
inputChange$ = new Subject<string>();
|
||||
@@ -53,11 +43,7 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private get numberOfResultsFetched(): number {
|
||||
if (
|
||||
!this.searchResult$ ||
|
||||
!this.searchResult$.value ||
|
||||
!Array.isArray(this.searchResult$.value.result)
|
||||
) {
|
||||
if (!this.searchResult$ || !this.searchResult$.value || !Array.isArray(this.searchResult$.value.result)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -65,17 +51,15 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private get totalResults(): number {
|
||||
if (
|
||||
!this.searchResult$ ||
|
||||
!this.searchResult$.value ||
|
||||
!this.searchResult$.value.hits
|
||||
) {
|
||||
if (!this.searchResult$ || !this.searchResult$.value || !this.searchResult$.value.hits) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.searchResult$.value.hits;
|
||||
}
|
||||
|
||||
showFilters = false;
|
||||
|
||||
constructor(
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
@@ -94,17 +78,19 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private initAvailableFilters() {
|
||||
this.availableFilters$ = this.customerSearch.getFilters().pipe(
|
||||
map((result) => {
|
||||
if (result.error) {
|
||||
} else {
|
||||
return result.result.map((input) =>
|
||||
this.filterMapping.fromInputDto(input)
|
||||
);
|
||||
}
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
this.customerSearch
|
||||
.getFilters()
|
||||
.pipe(
|
||||
map((result) => {
|
||||
if (result.error) {
|
||||
} else {
|
||||
const filters = result.result.map((input) => this.filterMapping.fromInputDto(input));
|
||||
this.setFilter(filters);
|
||||
return filters;
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
setQueryParams(queryParams: Params) {
|
||||
@@ -125,16 +111,26 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
startSearch() {
|
||||
if (
|
||||
!this.queryFilter.query ||
|
||||
(!this.queryFilter.query.length && this.environmentService.isMobile())
|
||||
) {
|
||||
if (!this.queryFilter.query || (!this.queryFilter.query.length && this.environmentService.isMobile())) {
|
||||
return this.scan();
|
||||
}
|
||||
|
||||
return this.search();
|
||||
}
|
||||
|
||||
getSelecteFiltersAsDictionary(): StringDictionary<string> {
|
||||
const dict: StringDictionary<string> = {};
|
||||
|
||||
for (const filter of this.queryFilter.filters) {
|
||||
const selected = filter.options.filter((o) => o.selected);
|
||||
if (selected.length > 0) {
|
||||
dict[filter.key] = selected.map((o) => o.id).join(';');
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
search(
|
||||
options: { isNewSearch: boolean; take?: number } = {
|
||||
isNewSearch: true,
|
||||
@@ -157,15 +153,14 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
? result.hits - this.numberOfResultsFetched
|
||||
: options.take;
|
||||
|
||||
this.searchResult$.next(
|
||||
this.addLoadingProducts(this.searchResult, effectiveTake)
|
||||
);
|
||||
this.searchResult$.next(this.addLoadingProducts(this.searchResult, effectiveTake));
|
||||
}),
|
||||
switchMap(() => {
|
||||
return this.customerSearch
|
||||
.getCustomers(this.queryFilter.query, {
|
||||
skip: options.isNewSearch ? 0 : this.numberOfResultsFetched,
|
||||
take: options.take,
|
||||
filter: this.getSelecteFiltersAsDictionary(),
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
@@ -190,9 +185,7 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
})
|
||||
)
|
||||
.subscribe((r) => {
|
||||
this.searchResult$.next(
|
||||
this.removeLoadingProducts(this.searchResult)
|
||||
);
|
||||
this.searchResult$.next(this.removeLoadingProducts(this.searchResult));
|
||||
if (options.isNewSearch) {
|
||||
this.searchResult$.next({
|
||||
...r,
|
||||
@@ -208,12 +201,13 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.searchResult$.next({
|
||||
...r,
|
||||
result: [
|
||||
...this.searchResult$.value.result,
|
||||
...r.result.map((a) => ({ ...a, loaded: true })),
|
||||
],
|
||||
result: [...this.searchResult$.value.result, ...r.result.map((a) => ({ ...a, loaded: true }))],
|
||||
});
|
||||
}
|
||||
|
||||
if (r.hits > 0) {
|
||||
this.showFilters = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -255,7 +249,7 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (!hasChanges) {
|
||||
if (current.filters.length !== next.filters.length) {
|
||||
if (current.filters !== next.filters) {
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
@@ -271,26 +265,18 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
|
||||
): PagedResult<CustomerSearchType> {
|
||||
return {
|
||||
...currentResult,
|
||||
result: [
|
||||
...currentResult.result,
|
||||
...new Array(numberOfLoadingProducts).fill({ loaded: false }),
|
||||
],
|
||||
result: [...currentResult.result, ...new Array(numberOfLoadingProducts).fill({ loaded: false })],
|
||||
};
|
||||
}
|
||||
|
||||
private removeLoadingProducts(
|
||||
currentResult: PagedResult<CustomerSearchType>
|
||||
): PagedResult<CustomerSearchType> {
|
||||
private removeLoadingProducts(currentResult: PagedResult<CustomerSearchType>): PagedResult<CustomerSearchType> {
|
||||
return {
|
||||
...currentResult,
|
||||
result: currentResult.result.filter((r) => !!r.loaded),
|
||||
};
|
||||
}
|
||||
|
||||
private shouldFetchNewProducts(options: {
|
||||
isNewSearch: boolean;
|
||||
take?: number;
|
||||
}): boolean {
|
||||
private shouldFetchNewProducts(options: { isNewSearch: boolean; take?: number }): boolean {
|
||||
if (options.isNewSearch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<page-customer-searchbox></page-customer-searchbox>
|
||||
<page-customer-searchbox (searchStarted)="applyFilters(false)"></page-customer-searchbox>
|
||||
|
||||
<ng-container *ngIf="this.search.availableFilters$ | async as filters">
|
||||
<ui-selected-filter-options [value]="selectedFilters"></ui-selected-filter-options>
|
||||
<ui-selected-filter-options [(value)]="selectedFilters" (valueChange)="updateFilter($event)"> </ui-selected-filter-options>
|
||||
|
||||
<ui-filter-group [value]="search.queryFilter.filters" (valueChanged)="updateFilter($event)"> </ui-filter-group>
|
||||
</ng-container>
|
||||
<ui-filter-group [(value)]="selectedFilters" (valueChange)="updateFilter($event)"></ui-filter-group>
|
||||
|
||||
<div class="sticky-cta-wrapper">
|
||||
<button class="apply-filter" (click)="applyFilters()">Filter anwenden</button>
|
||||
<button
|
||||
class="apply-filter"
|
||||
[class.loading]="search.searchState === 'fetching'"
|
||||
(click)="applyFilters()"
|
||||
[disabled]="search.searchState === 'fetching'"
|
||||
>
|
||||
<span *ngIf="search.searchState !== 'fetching'">
|
||||
Filter anwenden
|
||||
</span>
|
||||
<ui-icon class="spin" *ngIf="search.searchState === 'fetching'" icon="loading" size="26px"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -9,4 +9,22 @@
|
||||
|
||||
button.apply-filter {
|
||||
@apply border-none bg-brand text-white rounded-full py-cta-y-l px-cta-x-l text-cta-l font-bold;
|
||||
min-width: 201px;
|
||||
}
|
||||
|
||||
button.apply-filter.loading {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
||||
ui-selected-filter-options {
|
||||
@apply my-px-8;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply inline-flex;
|
||||
}
|
||||
|
||||
.spin {
|
||||
@apply animate-spin;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnDestroy } from '@angular/core';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { CustomerSearch } from '../customer-search.service';
|
||||
|
||||
@@ -13,24 +8,32 @@ import { CustomerSearch } from '../customer-search.service';
|
||||
styleUrls: ['search-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CustomerSearchFilterComponent implements OnInit {
|
||||
export class CustomerSearchFilterComponent implements OnInit, OnDestroy {
|
||||
selectedFilters: Filter[];
|
||||
initialFilter: Filter[];
|
||||
|
||||
constructor(public search: CustomerSearch, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.selectedFilters = this.search.queryFilter.filters;
|
||||
this.initialFilter = this.search.queryFilter.filters;
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
// 1. Filter setzen
|
||||
// 2. Suchen
|
||||
// 3. Weiterleitung
|
||||
ngOnDestroy() {
|
||||
this.search.setFilter(this.initialFilter);
|
||||
}
|
||||
|
||||
applyFilters(search: boolean = true) {
|
||||
this.search.setFilter(this.selectedFilters);
|
||||
this.initialFilter = this.selectedFilters;
|
||||
|
||||
if (search) {
|
||||
this.search.search({ isNewSearch: true });
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(filters: Filter[]) {
|
||||
this.selectedFilters = [...filters.map((c) => ({ ...c }))];
|
||||
this.search.setFilter(filters);
|
||||
this.cdr.markForCheck();
|
||||
console.log(filters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
uiSearchboxInput
|
||||
placeholder="Name, E-Mail, Kundenkartennummer, ..."
|
||||
(inputChange)="search.inputChange$.next($event)"
|
||||
(keydown.enter)="search.startSearch(); autocomplete.close()"
|
||||
(keydown.enter)="startSearch(); autocomplete.close()"
|
||||
/>
|
||||
<ui-searchbox-warning *ngIf="searchState === 'empty'">
|
||||
Keine Suchergebnisse
|
||||
@@ -19,7 +19,7 @@
|
||||
[class.scan]="isMobile && !input?.value?.length"
|
||||
type="submit"
|
||||
uiSearchboxSearchButton
|
||||
(click)="search.startSearch()"
|
||||
(click)="startSearch()"
|
||||
[disabled]="searchState === 'fetching'"
|
||||
>
|
||||
<ui-icon class="spin" *ngIf="searchState === 'fetching'" icon="spinner" size="32px"></ui-icon>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy, ViewChild, EventEmitter, Output } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { AutocompleteDTO } from '@swagger/crm';
|
||||
import { UiFilterMappingService } from '@ui/filter';
|
||||
import { UiSearchboxAutocompleteComponent } from '@ui/searchbox';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, delay, filter, map, shareReplay, skip, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
@@ -28,6 +29,9 @@ export class CustomerSearchboxComponent implements OnInit, OnDestroy {
|
||||
|
||||
queryControl = new FormControl(this.search.queryFilter.query || '');
|
||||
|
||||
@Output()
|
||||
searchStarted = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
public search: CustomerSearch,
|
||||
private environmentService: EnvironmentService,
|
||||
@@ -35,6 +39,11 @@ export class CustomerSearchboxComponent implements OnInit, OnDestroy {
|
||||
private crmCustomerService: CrmCustomerService
|
||||
) {}
|
||||
|
||||
startSearch() {
|
||||
this.search.startSearch();
|
||||
this.searchStarted.emit();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.detectDevice();
|
||||
this.initAutocomplete();
|
||||
@@ -53,7 +62,7 @@ export class CustomerSearchboxComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroy$),
|
||||
switchMap((queryString) => {
|
||||
if (queryString.length >= 3) {
|
||||
return this.crmCustomerService.complete(queryString).pipe(
|
||||
return this.crmCustomerService.complete(queryString, this.search.getSelecteFiltersAsDictionary()).pipe(
|
||||
map((response) => response.result),
|
||||
catchError(() => {
|
||||
// TODO Dialog Service impl. fur Anzeige der Fehlermeldung
|
||||
@@ -65,7 +74,6 @@ export class CustomerSearchboxComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
tap((results) => {
|
||||
console.log({ results, autocomplete: this.autocomplete });
|
||||
if (this.autocomplete) {
|
||||
if (results.length > 0) {
|
||||
this.autocomplete.open();
|
||||
|
||||
@@ -3,26 +3,19 @@
|
||||
<button
|
||||
*ngFor="let filter of value; trackBy: trackByKeyOrName; let first = first"
|
||||
type="button"
|
||||
(click)="selected = filter"
|
||||
(click)="active = filter"
|
||||
class="isa-btn isa-btn-block isa-btn-default-bg isa-btn-large"
|
||||
[class.customer]="module === 'Customer'"
|
||||
[class.isa-mt-10]="!first"
|
||||
[class.is-active]="filter === selected"
|
||||
[class.isa-font-weight-emphasis]="filter !== selected"
|
||||
[class.isa-font-weight-bold]="filter === selected"
|
||||
[class.is-active]="filter?.key === active?.key"
|
||||
[class.isa-font-weight-emphasis]="filter?.key !== active?.key"
|
||||
[class.isa-font-weight-bold]="filter?.key === active?.key"
|
||||
>
|
||||
<span>{{ filter.name }}</span>
|
||||
<ui-icon size="17px" icon="arrow_head"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-content isa-ml-10" [ngSwitch]="selected?.type">
|
||||
<ui-select-filter
|
||||
*ngSwitchCase="'select'"
|
||||
[options]="selected.options"
|
||||
[module]="module"
|
||||
[max]="selected.max"
|
||||
(optionsChanged)="emitOnChange($event)"
|
||||
>
|
||||
</ui-select-filter>
|
||||
<div class="filter-content isa-ml-10" [ngSwitch]="active?.type">
|
||||
<ui-select-filter *ngSwitchCase="'select'" [filter]="active" [module]="module"> </ui-select-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { isArray } from '@utils/common';
|
||||
import { Filter, SelectFilterOption } from '../../../models';
|
||||
import {} from '@utils/common';
|
||||
import { Filter } from '../../../models';
|
||||
import { cloneFilter } from '../../../utils';
|
||||
import { FilterGroup } from '../../filter-group';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-filter-group',
|
||||
@@ -25,24 +26,42 @@ import { cloneFilter } from '../../../utils';
|
||||
useExisting: forwardRef(() => UiFilterGroupComponent),
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: FilterGroup,
|
||||
useExisting: forwardRef(() => UiFilterGroupComponent),
|
||||
},
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterGroupComponent implements OnInit, ControlValueAccessor, OnChanges {
|
||||
export class UiFilterGroupComponent extends FilterGroup implements OnInit, ControlValueAccessor, OnChanges {
|
||||
@Input()
|
||||
disabled: boolean;
|
||||
|
||||
@Input()
|
||||
value: Filter[];
|
||||
|
||||
@Output()
|
||||
valueChanged = new EventEmitter<Filter[]>();
|
||||
|
||||
@Output()
|
||||
valueGroupChanged = new EventEmitter<Filter>();
|
||||
private _value: Filter[];
|
||||
|
||||
@Input()
|
||||
selected: Filter;
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
set value(val) {
|
||||
this._value = val.map(cloneFilter);
|
||||
this.updateView.emit();
|
||||
}
|
||||
|
||||
@Output()
|
||||
valueChange = new EventEmitter<Filter[]>();
|
||||
|
||||
@Input()
|
||||
active: Filter;
|
||||
|
||||
// @Input()
|
||||
// value: Filter[];
|
||||
|
||||
// @Output()
|
||||
// valueChanged = new EventEmitter<Filter[]>();
|
||||
|
||||
// @Output()
|
||||
// valueGroupChanged = new EventEmitter<Filter>();
|
||||
|
||||
@Input() module: 'Customer' | 'Branch' = 'Branch';
|
||||
|
||||
@@ -52,22 +71,32 @@ export class UiFilterGroupComponent implements OnInit, ControlValueAccessor, OnC
|
||||
|
||||
trackByKeyOrName = (filter: Filter) => filter.key || filter.name;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
constructor(protected cdr: ChangeDetectorRef) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnChanges({ value, selected }: SimpleChanges): void {
|
||||
if (value && isArray(value.currentValue)) {
|
||||
this.value = value.currentValue.map(cloneFilter);
|
||||
}
|
||||
if (value || selected) {
|
||||
console.log({ value, selected });
|
||||
const selectedFilter = this.getSelectedFilter();
|
||||
if (selectedFilter) {
|
||||
this.selected = selectedFilter.filter;
|
||||
}
|
||||
ngOnChanges({ value }: SimpleChanges) {
|
||||
if (value && value.isFirstChange() && !this.active) {
|
||||
this.active = this.value[0];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
// ngOnChanges({ value, selected }: SimpleChanges): void {
|
||||
// if (value && isArray(value.currentValue)) {
|
||||
// this.value = value.currentValue.map(cloneFilter);
|
||||
// }
|
||||
// if (value || selected) {
|
||||
// console.log({ value, selected });
|
||||
// const selectedFilter = this.getSelectedFilter();
|
||||
// if (selectedFilter) {
|
||||
// this.selected = selectedFilter.filter;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
ngOnInit() {
|
||||
this.updateView.subscribe((_) => this.cdr.markForCheck());
|
||||
}
|
||||
|
||||
writeValue(obj: Filter[]): void {
|
||||
if (obj !== this.value) {
|
||||
@@ -88,26 +117,26 @@ export class UiFilterGroupComponent implements OnInit, ControlValueAccessor, OnC
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
emitOnChange(changedOptionGroup: SelectFilterOption[]) {
|
||||
const selected = this.getSelectedFilter();
|
||||
selected.filter.options = changedOptionGroup;
|
||||
// emitOnChange(changedOptionGroup: SelectFilterOption[]) {
|
||||
// const selected = this.getSelectedFilter();
|
||||
// selected.filter.options = changedOptionGroup;
|
||||
|
||||
if (typeof this.onChange === 'function') {
|
||||
this.onChange(this.value);
|
||||
}
|
||||
this.valueChanged.emit(this.value);
|
||||
this.valueGroupChanged.emit(selected.filter);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
// if (typeof this.onChange === 'function') {
|
||||
// this.onChange(this.value);
|
||||
// }
|
||||
// this.valueChanged.emit(this.value);
|
||||
// this.valueGroupChanged.emit(selected.filter);
|
||||
// this.cdr.markForCheck();
|
||||
// }
|
||||
|
||||
getSelectedFilter() {
|
||||
if (this.value && this.selected) {
|
||||
const index = this.value.findIndex((f) => this.selected.key === f.key || this.selected.name === f.name);
|
||||
const filter = this.value[index];
|
||||
// getSelectedFilter() {
|
||||
// if (this.value && this.selected) {
|
||||
// const index = this.value.findIndex((f) => this.selected.key === f.key || this.selected.name === f.name);
|
||||
// const filter = this.value[index];
|
||||
|
||||
return { index, filter };
|
||||
}
|
||||
// return { index, filter };
|
||||
// }
|
||||
|
||||
return undefined;
|
||||
}
|
||||
// return undefined;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
ChangeDetectorRef,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { SelectFilterOption } from '../../../models';
|
||||
import { SelectFilter, SelectFilterOption } from '../../../models';
|
||||
import { cloneSelectFilterOption } from '../../../utils';
|
||||
import { FilterGroup } from '../../filter-group';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-select-filter-option',
|
||||
@@ -17,22 +19,28 @@ import { cloneSelectFilterOption } from '../../../utils';
|
||||
styleUrls: ['select-filter-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiSelectFilterOptionComponent implements OnChanges {
|
||||
export class UiSelectFilterOptionComponent implements OnInit, OnChanges {
|
||||
@Input()
|
||||
filter: SelectFilter;
|
||||
|
||||
@Input()
|
||||
option: SelectFilterOption;
|
||||
|
||||
@Output()
|
||||
optionChanged = new EventEmitter<SelectFilterOption>();
|
||||
|
||||
@Input() module: 'Customer' | 'Branch' = 'Branch';
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
constructor(private cdr: ChangeDetectorRef, private filterGroup: FilterGroup) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.filterGroup.updateView.subscribe((_) => {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges({ option }: SimpleChanges): void {
|
||||
if (option) {
|
||||
this.option = cloneSelectFilterOption(this.option);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
// if (option) {
|
||||
// this.option = cloneSelectFilterOption(this.option);
|
||||
// this.cdr.markForCheck();
|
||||
// }
|
||||
}
|
||||
|
||||
toggle(
|
||||
@@ -42,45 +50,42 @@ export class UiSelectFilterOptionComponent implements OnChanges {
|
||||
updateChildren: true,
|
||||
}
|
||||
) {
|
||||
this.option.selected = typeof val === 'boolean' ? val : !this.option.selected;
|
||||
|
||||
if (updateChildren && Array.isArray(this.option.options)) {
|
||||
const selectFn = (option: SelectFilterOption): SelectFilterOption => {
|
||||
return {
|
||||
...option,
|
||||
selected: this.option.selected,
|
||||
options: Array.isArray(option.options) ? option.options.map(selectFn) : option.options,
|
||||
};
|
||||
};
|
||||
this.option.options = this.option.options.map(selectFn);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: no-unused-expression
|
||||
emitChanges && this.onChange();
|
||||
this.filterGroup.toggleOption(this.filter, this.option);
|
||||
this.cdr.markForCheck();
|
||||
// this.option.selected = typeof val === 'boolean' ? val : !this.option.selected;
|
||||
// if (updateChildren && Array.isArray(this.option.options)) {
|
||||
// const selectFn = (option: SelectFilterOption): SelectFilterOption => {
|
||||
// return {
|
||||
// ...option,
|
||||
// selected: this.option.selected,
|
||||
// options: Array.isArray(option.options) ? option.options.map(selectFn) : option.options,
|
||||
// };
|
||||
// };
|
||||
// this.option.options = this.option.options.map(selectFn);
|
||||
// }
|
||||
// // tslint:disable-next-line: no-unused-expression
|
||||
// emitChanges && this.onChange();
|
||||
// this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.optionChanged.emit(this.option);
|
||||
// this.optionChanged.emit(this.option);
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
this.option.expanded = !this.option.expanded;
|
||||
this.onChange();
|
||||
this.cdr.markForCheck();
|
||||
// this.option.expanded = !this.option.expanded;
|
||||
// this.onChange();
|
||||
// this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
childChanged(option: SelectFilterOption, index: number) {
|
||||
this.option.options[index] = option;
|
||||
|
||||
const selectedTrueChilds = this.option.options.filter((child) => !!child.selected);
|
||||
const selected = selectedTrueChilds.length === this.option.options.length;
|
||||
|
||||
if (this.option.selected !== selected) {
|
||||
this.option.selected = selected;
|
||||
}
|
||||
|
||||
this.onChange();
|
||||
this.cdr.markForCheck();
|
||||
// this.option.options[index] = option;
|
||||
// const selectedTrueChilds = this.option.options.filter((child) => !!child.selected);
|
||||
// const selected = selectedTrueChilds.length === this.option.options.length;
|
||||
// if (this.option.selected !== selected) {
|
||||
// this.option.selected = selected;
|
||||
// }
|
||||
// this.onChange();
|
||||
// this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
class="isa-btn isa-p-0 isa-font-weight-emphasis"
|
||||
[class.isa-font-color-customer]="module === 'Customer'"
|
||||
[class.isa-font-lightgrey]="module !== 'Customer'"
|
||||
*ngIf="!max"
|
||||
*ngIf="!filter?.max"
|
||||
(click)="selectAll()"
|
||||
>
|
||||
{{ (allSelected$ | async) ? 'Alle entfernen' : 'Alle auswählen' }}
|
||||
{{ filterGroup.isAllSelected(filter) ? 'Alle entfernen' : 'Alle auswählen' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="select-filter-options isa-mt-10">
|
||||
<div class="select-filter-options-content" (scroll)="updateScrollButton()" #optionsContainer (cdkObserveContent)="updateScrollButton()">
|
||||
<ng-container *ngFor="let option of options; let index = index">
|
||||
<ui-select-filter-option [module]="module" [option]="option" (optionChanged)="optionChanged($event, index)">
|
||||
</ui-select-filter-option>
|
||||
<ng-container *ngFor="let option of filterGroup.getFilterRef(filter)?.options; let index = index">
|
||||
<ui-select-filter-option [module]="module" [filter]="filter" [option]="option"> </ui-select-filter-option>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
OnChanges,
|
||||
@@ -11,10 +9,11 @@ import {
|
||||
ChangeDetectorRef,
|
||||
AfterViewInit,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { SelectFilterOption } from '../../../models';
|
||||
import { BehaviorSubject, interval, Subscription } from 'rxjs';
|
||||
import { cloneSelectFilterOption, flattenSelectFilterOption } from '../../../utils';
|
||||
import { SelectFilter, SelectFilterOption } from '../../../models';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { FilterGroup } from '../../filter-group';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-select-filter',
|
||||
@@ -22,15 +21,9 @@ import { cloneSelectFilterOption, flattenSelectFilterOption } from '../../../uti
|
||||
styleUrls: ['select-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiSelectFilterComponent implements OnChanges, AfterViewInit, OnDestroy {
|
||||
export class UiSelectFilterComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
|
||||
@Input()
|
||||
options: SelectFilterOption[];
|
||||
|
||||
@Output()
|
||||
optionsChanged = new EventEmitter<SelectFilterOption[]>();
|
||||
|
||||
@Input()
|
||||
max?: number;
|
||||
filter: SelectFilter;
|
||||
|
||||
@Input() module: 'Customer' | 'Branch' = 'Branch';
|
||||
|
||||
@@ -39,11 +32,14 @@ export class UiSelectFilterComponent implements OnChanges, AfterViewInit, OnDest
|
||||
|
||||
canScroll = false;
|
||||
scrollPositionPersantage = 0;
|
||||
allSelected$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
updateScrollPositionPersantageIntervalSub: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
constructor(protected cdr: ChangeDetectorRef, public filterGroup: FilterGroup) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.filterGroup.updateView.subscribe((_) => this.cdr.markForCheck());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.updateScrollPositionPersantageIntervalSub) {
|
||||
@@ -67,7 +63,6 @@ export class UiSelectFilterComponent implements OnChanges, AfterViewInit, OnDest
|
||||
const container = this.optionsContainer.nativeElement;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const height = container.clientHeight;
|
||||
|
||||
if (this.canScroll !== scrollHeight > height) {
|
||||
this.canScroll = scrollHeight > height;
|
||||
this.cdr.markForCheck();
|
||||
@@ -75,12 +70,12 @@ export class UiSelectFilterComponent implements OnChanges, AfterViewInit, OnDest
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges({ options }: SimpleChanges): void {
|
||||
if (options) {
|
||||
this.options = this.options.map(cloneSelectFilterOption);
|
||||
this.setAllSelected(options.currentValue);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
ngOnChanges({}: SimpleChanges): void {
|
||||
// if (options) {
|
||||
// this.options = this.options.map(cloneSelectFilterOption);
|
||||
// this.setAllSelected(options.currentValue);
|
||||
// this.cdr.markForCheck();
|
||||
// }
|
||||
}
|
||||
|
||||
createSelctFilterOptionContext(option: SelectFilterOption) {
|
||||
@@ -88,67 +83,16 @@ export class UiSelectFilterComponent implements OnChanges, AfterViewInit, OnDest
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const selectFn = (option: SelectFilterOption): SelectFilterOption => {
|
||||
return {
|
||||
...option,
|
||||
selected: !this.allSelected$.value,
|
||||
options: Array.isArray(option.options) ? option.options.map(selectFn) : option.options,
|
||||
};
|
||||
};
|
||||
|
||||
this.options = this.options.map(cloneSelectFilterOption).map(selectFn);
|
||||
|
||||
this.optionsChanged.emit(this.options);
|
||||
this.setAllSelected(this.options);
|
||||
this.filterGroup.toggleAll(this.filter);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
optionChanged(option: SelectFilterOption, index: number) {
|
||||
let options = [...this.options];
|
||||
options[index] = option;
|
||||
const selectedOptions = flattenSelectFilterOption(options).filter((o) => o.selected);
|
||||
|
||||
if (this.max > 0 && selectedOptions.length > this.max) {
|
||||
const unselectOptions = selectedOptions
|
||||
.filter((o) => !(o.key === option.key && o.name === option.name))
|
||||
.slice(0, selectedOptions.length - this.max);
|
||||
|
||||
const updateOption = (o: SelectFilterOption): SelectFilterOption =>
|
||||
unselectOptions.some((s) => s.key === o.key && s.name === o.name) ? { ...o, selected: false } : o;
|
||||
options = options.map(updateOption);
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.optionsChanged.emit(this.options);
|
||||
this.setAllSelected(this.options);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
setAllSelected(options: SelectFilterOption[]) {
|
||||
this.allSelected$.next(this.isAllSelected(options));
|
||||
}
|
||||
|
||||
private isAllSelected(options: SelectFilterOption[]): boolean {
|
||||
if (options.every((option) => !!option.selected)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return options.every((option) => {
|
||||
if (!option.options || !option.options.length) {
|
||||
return option.selected;
|
||||
}
|
||||
|
||||
return option.options.every((o) => o.selected);
|
||||
});
|
||||
}
|
||||
|
||||
updateScrollPositionPersantage() {
|
||||
if (this.optionsContainer) {
|
||||
const container: HTMLElement = this.optionsContainer.nativeElement;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const scrollTop = container.scrollTop;
|
||||
const height = container.clientHeight;
|
||||
|
||||
const scrollPositionPersantage = Math.round((100 / (scrollHeight - height)) * scrollTop);
|
||||
if (this.scrollPositionPersantage !== scrollPositionPersantage) {
|
||||
this.scrollPositionPersantage = scrollPositionPersantage;
|
||||
|
||||
58
apps/ui/filter/src/lib/filter-group/filter-group.ts
Normal file
58
apps/ui/filter/src/lib/filter-group/filter-group.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { Filter, FilterOption } from '../models';
|
||||
import { isBoolean } from '@utils/common';
|
||||
|
||||
export abstract class FilterGroup {
|
||||
abstract value: Filter[];
|
||||
|
||||
abstract valueChange: EventEmitter<Filter[]>;
|
||||
|
||||
abstract active: Filter;
|
||||
|
||||
updateView = new EventEmitter();
|
||||
|
||||
toggleAll(filter: Filter) {
|
||||
const allSelected = this.isAllSelected(filter);
|
||||
if (isBoolean(allSelected)) {
|
||||
const current = this.getFilterRef(filter);
|
||||
current.options.forEach((option) => (option.selected = !allSelected));
|
||||
this.valueChange.emit(this.value);
|
||||
this.updateView.emit();
|
||||
}
|
||||
}
|
||||
|
||||
toggleOption(filter: Filter, option: FilterOption) {
|
||||
const optionRef = this.getOptionRef(filter, option);
|
||||
if (optionRef) {
|
||||
optionRef.selected = !optionRef.selected;
|
||||
this.valueChange.emit(this.value);
|
||||
this.updateView.emit();
|
||||
}
|
||||
}
|
||||
|
||||
isAllSelected(filter: Filter) {
|
||||
if (this.value && filter) {
|
||||
const filterRef = this.getFilterRef(filter);
|
||||
return filterRef.options.filter((f) => f.selected).length === filterRef.options.length;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getFilterRef(filter: Filter) {
|
||||
return this.value.find((f) => f.key === filter.key);
|
||||
}
|
||||
|
||||
getOptionRef(filter: Filter, option: FilterOption) {
|
||||
const filterRef = this.getFilterRef(filter);
|
||||
|
||||
if (filterRef) {
|
||||
const currentOption = filterRef.options.find((o) => o.id === option.id);
|
||||
|
||||
if (currentOption) {
|
||||
return currentOption;
|
||||
}
|
||||
|
||||
//TODO: Find Sub Option Ref
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
<div class="container" *ngIf="value">
|
||||
<div class="clear-all" *ngIf="showResetButton">
|
||||
<button class="isa-btn isa-btn-primary-link remove-all isa-pt-0 isa-pb-0 isa-pl-15 isa-pr-15" (click)="resetFilters()">
|
||||
{{ resetStrategy | filterResetWording }}
|
||||
<button class="filters-clear" *ngIf="selectedOptions?.length" (click)="unselectAllOptions()">
|
||||
Alle Filter Entfernen
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="filter-option"
|
||||
*ngFor="let option of selectedOptions; let optionIndex = index"
|
||||
[class.collapsed]="collapsed && optionIndex >= collapseAtIndex"
|
||||
>
|
||||
<span class="option-name">{{ option.name }}</span>
|
||||
<button class="option-clear" (click)="unselectOption(option)">
|
||||
<ui-icon icon="close" size="15px"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let filter of value">
|
||||
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: filter }"></ng-container>
|
||||
</ng-container>
|
||||
<button class="display-more" (click)="collapsed = false" *ngIf="collapsed && selectedOptions?.length > collapseAtIndex">
|
||||
Mehr
|
||||
<ui-icon size="15px" icon="arrow"></ui-icon>
|
||||
</button>
|
||||
|
||||
<ng-container [ngSwitch]="isCollapsed" *ngIf="items && items.length > collapsedStateNumberOfSelectedFiltersShown">
|
||||
<div class="expand c-pointer" *ngSwitchCase="true" (click)="updateCollapsed(false)">
|
||||
<span [class.isa-font-color-customer]="module === 'Customer'">Mehr</span>
|
||||
<ui-icon size="20px" icon="arrow"></ui-icon>
|
||||
</div>
|
||||
<div class="collapse c-pointer" *ngSwitchDefault (click)="updateCollapsed(true)">
|
||||
<ui-icon size="20px" icon="arrow" rotate="-180deg"></ui-icon>
|
||||
<span [class.isa-font-color-customer]="module === 'Customer'">Weniger</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button class="display-less" (click)="collapsed = true" *ngIf="!collapsed && selectedOptions?.length > collapseAtIndex">
|
||||
<ui-icon size="15px" icon="arrow" rotate="-180deg"></ui-icon>
|
||||
Weniger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template #optionTmpl let-filter>
|
||||
<div *ngIf="filter.selected" class="selected-filter" #filteredItem>
|
||||
<span class="filter-name">{{ filter.name }}</span>
|
||||
<ui-icon (click)="clearFilter(filter); (false)" icon="close"></ui-icon>
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let option of filter.options | checkAllChildOptionsSelected">
|
||||
<ng-container *ngTemplateOutlet="optionTmpl; context: { $implicit: option }"></ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,103 +1,31 @@
|
||||
$color-grey: #596470;
|
||||
$filter-padding-right: 13px;
|
||||
$icon-margin: 6px;
|
||||
$font-size: 18px;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 17px;
|
||||
margin-bottom: 17px;
|
||||
justify-content: center;
|
||||
padding: 0 2rem;
|
||||
|
||||
.filter,
|
||||
.clear-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selected-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clear-all {
|
||||
.filter-name {
|
||||
padding-right: $filter-padding-right;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
font-weight: bold;
|
||||
font-size: $font-size;
|
||||
line-height: 2;
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.remove-all {
|
||||
font-weight: bold;
|
||||
line-height: 2;
|
||||
font-size: $font-size;
|
||||
cursor: pointer;
|
||||
color: #f70400;
|
||||
}
|
||||
|
||||
.selected-filter {
|
||||
margin-right: $filter-padding-right;
|
||||
.filter-name {
|
||||
padding-right: $filter-padding-right;
|
||||
}
|
||||
}
|
||||
|
||||
lib-icon {
|
||||
margin-top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expand,
|
||||
.collapse {
|
||||
margin-left: $icon-margin;
|
||||
font-weight: bold;
|
||||
font-size: $font-size;
|
||||
color: $color-grey;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expand {
|
||||
margin-left: 16px;
|
||||
lib-icon {
|
||||
margin-left: $icon-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
lib-icon {
|
||||
margin-right: $icon-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
@apply hidden;
|
||||
}
|
||||
@apply flex flex-row justify-center items-center flex-wrap gap-3 my-px-20;
|
||||
}
|
||||
|
||||
::ng-deep ui-selected-filter-options .expand > ui-icon,
|
||||
::ng-deep ui-selected-filter-options .collapse > ui-icon,
|
||||
::ng-deep [theme='customer'] ui-selected-filter-options .expand > ui-icon,
|
||||
::ng-deep [theme='customer'] ui-selected-filter-options .collapse > ui-icon {
|
||||
@apply text-ucla-blue;
|
||||
.container.collapsed {
|
||||
}
|
||||
|
||||
::ng-deep ui-selected-filter-options .selected-filter > ui-icon,
|
||||
::ng-deep [theme='customer'] ui-selected-filter-options .selected-filter > ui-icon {
|
||||
@apply text-ucla-blue;
|
||||
.filters-clear {
|
||||
@apply bg-transparent text-brand border-none outline-none text-cta-l font-bold;
|
||||
}
|
||||
|
||||
::ng-deep [theme='branch'] ui-selected-filter-options .selected-filter > ui-icon {
|
||||
@apply text-cool-grey;
|
||||
.filter-option {
|
||||
@apply flex flex-row items-baseline text-cta-l font-bold gap-2;
|
||||
}
|
||||
|
||||
.filter-option.collapsed {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.option-clear {
|
||||
@apply bg-transparent border-none outline-none text-ucla-blue;
|
||||
}
|
||||
|
||||
.display-more,
|
||||
.display-less {
|
||||
@apply flex items-baseline gap-2 bg-transparent text-ucla-blue border-none outline-none text-cta-l font-bold;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
ChangeDetectorRef,
|
||||
AfterViewInit,
|
||||
Renderer2,
|
||||
ElementRef,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
import { Filter, SelectFilterOption } from '../../models';
|
||||
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, ChangeDetectorRef } from '@angular/core';
|
||||
import { Filter, FilterOption } from '../../models';
|
||||
import { cloneFilter } from '../../utils';
|
||||
import { FilterResetStrategy } from './defs';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-selected-filter-options',
|
||||
@@ -22,160 +8,58 @@ import { FilterResetStrategy } from './defs';
|
||||
styleUrls: ['./selected-filter-options.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiSelectedFilterOptionsComponent implements AfterViewInit, OnChanges {
|
||||
@Input() value: Filter[];
|
||||
@Input() module: 'Customer' | 'Branch' = 'Branch';
|
||||
@Input() resetStrategy: FilterResetStrategy = 'all';
|
||||
@Input() showResetButton = true;
|
||||
export class UiSelectedFilterOptionsComponent {
|
||||
private _value: Filter[];
|
||||
|
||||
@Output() filterChanged = new EventEmitter<Filter[]>();
|
||||
@Output() reset = new EventEmitter<void>();
|
||||
@Input()
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
set value(val) {
|
||||
this._value = val.map(cloneFilter);
|
||||
|
||||
@ViewChildren('filteredItem') items: QueryList<ElementRef>;
|
||||
this.selectedOptions = this.value.reduce((aggr, filter) => {
|
||||
const selected = filter.options.filter((f) => f.selected);
|
||||
return [...aggr, ...selected];
|
||||
}, []);
|
||||
|
||||
collapsedStateNumberOfSelectedFiltersShown = 3;
|
||||
isCollapsed = true;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private renderer: Renderer2) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.items.changes.subscribe((_) => {
|
||||
this.cdr.detectChanges();
|
||||
|
||||
this.updateCollapsed(this.isCollapsed);
|
||||
});
|
||||
|
||||
setTimeout(() => this.cdr.detectChanges(), 0);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
ngOnChanges(changes) {
|
||||
console.log(changes);
|
||||
}
|
||||
@Output()
|
||||
valueChange = new EventEmitter<Filter[]>();
|
||||
|
||||
updateCollapsed(isCollapsed: boolean) {
|
||||
this.isCollapsed = isCollapsed;
|
||||
selectedOptions: FilterOption[] = [];
|
||||
|
||||
this.items.forEach((item, index) => {
|
||||
if (this.isCollapsed && index >= this.collapsedStateNumberOfSelectedFiltersShown) {
|
||||
this.renderer.addClass(item.nativeElement, 'collapsed');
|
||||
} else {
|
||||
this.renderer.removeClass(item.nativeElement, 'collapsed');
|
||||
}
|
||||
});
|
||||
collapsed = true;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
collapseAtIndex = 3;
|
||||
|
||||
resetFilters() {
|
||||
switch (this.resetStrategy) {
|
||||
case 'default':
|
||||
this.filterChanged.emit(this.resetToDefault(this.value));
|
||||
break;
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
case 'all':
|
||||
this.filterChanged.emit(this.deselectAllFilters(this.value));
|
||||
break;
|
||||
}
|
||||
|
||||
this.reset.emit();
|
||||
}
|
||||
|
||||
clearFilter(filter: SelectFilterOption) {
|
||||
let clonedFilter = this.value.map(cloneFilter);
|
||||
const deselect = (sf: SelectFilterOption) => {
|
||||
if (sf.name === filter.name) {
|
||||
return this.deselect(sf);
|
||||
}
|
||||
|
||||
if (Array.isArray(sf.options)) {
|
||||
deselectIfMatch(sf.options);
|
||||
}
|
||||
};
|
||||
|
||||
function deselectIfMatch(selectFilterOptions: SelectFilterOption[]) {
|
||||
selectFilterOptions.forEach((option) => {
|
||||
if (option.name === filter.name) {
|
||||
deselect(option);
|
||||
}
|
||||
|
||||
if (Array.isArray(option.options)) {
|
||||
deselectIfMatch(option.options);
|
||||
}
|
||||
unselectAllOptions() {
|
||||
function unselectOptions(options: FilterOption[]) {
|
||||
options?.forEach((option) => {
|
||||
option.selected = false;
|
||||
unselectOptions(option.options);
|
||||
});
|
||||
}
|
||||
|
||||
clonedFilter = clonedFilter
|
||||
.map((f) => {
|
||||
if (Array.isArray(f.options)) {
|
||||
f.options = f.options.map((o) => {
|
||||
deselect(o);
|
||||
this.value?.forEach((filter) => unselectOptions(filter.options));
|
||||
this.valueChange.emit(this.value);
|
||||
}
|
||||
|
||||
return o;
|
||||
});
|
||||
unselectOption(option: FilterOption) {
|
||||
function unselectOptions(options: FilterOption[]) {
|
||||
options?.forEach((o) => {
|
||||
if (o.id === option.id) {
|
||||
o.selected = false;
|
||||
}
|
||||
|
||||
return f;
|
||||
})
|
||||
.map(cloneFilter);
|
||||
|
||||
this.filterChanged.emit(clonedFilter);
|
||||
}
|
||||
|
||||
deselectAllFilters(filters: Filter[]): Filter[] {
|
||||
return filters.reduce((acc, curr) => {
|
||||
if (Array.isArray(curr.options)) {
|
||||
curr.options.forEach((option) => this.deselect(option));
|
||||
}
|
||||
return acc.concat({ ...curr });
|
||||
}, []);
|
||||
}
|
||||
|
||||
resetToDefault(filters: Filter[]): Filter[] {
|
||||
const updatedFilters = filters.map((filter) => {
|
||||
let updatedOptions = filter.options || [];
|
||||
if (filter.options) {
|
||||
updatedOptions = filter.options.map((f) => ({
|
||||
...this.resetToDefaultValue(f),
|
||||
}));
|
||||
}
|
||||
|
||||
return { ...filter, options: updatedOptions };
|
||||
});
|
||||
|
||||
return updatedFilters;
|
||||
}
|
||||
|
||||
private deselectAllMatches(filter: SelectFilterOption, match?: string) {
|
||||
if (filter.name === match) {
|
||||
filter.selected = false;
|
||||
return filter.options.forEach((option) => this.deselect(option));
|
||||
unselectOptions(o.options);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasNestedOptions(filter)) {
|
||||
filter.options.forEach((option) => option.name === match && this.deselectAllMatches(option));
|
||||
}
|
||||
}
|
||||
|
||||
private deselect(filter: SelectFilterOption, match?: string): SelectFilterOption {
|
||||
if (this.hasNestedOptions(filter)) {
|
||||
filter.options.forEach((option) => this.deselect(option));
|
||||
}
|
||||
|
||||
if (filter.name === match || !match) {
|
||||
filter.selected = false;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
private resetToDefaultValue(filter: SelectFilterOption): SelectFilterOption {
|
||||
return {
|
||||
...filter,
|
||||
selected: filter.initial_selected_state,
|
||||
options: [...(filter.options ? filter.options.map((option) => this.resetToDefaultValue(option)) : [])],
|
||||
};
|
||||
}
|
||||
|
||||
private hasNestedOptions(filter: SelectFilterOption): boolean {
|
||||
return filter && Array.isArray(filter.options) && !!filter.options.length;
|
||||
this.value.forEach((filter) => unselectOptions(filter.options));
|
||||
this.valueChange.emit(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
} from '@angular/core';
|
||||
import { Directive, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, Output, Renderer2 } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Directive({
|
||||
@@ -46,7 +36,6 @@ export class UiSearchboxInputDirective implements ControlValueAccessor {
|
||||
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
console.log(obj);
|
||||
this.setValue(obj, { inputType: 'init' });
|
||||
}
|
||||
|
||||
@@ -73,10 +62,7 @@ export class UiSearchboxInputDirective implements ControlValueAccessor {
|
||||
this.setValue(value, { inputType: 'input' });
|
||||
}
|
||||
|
||||
setValue(
|
||||
value: string,
|
||||
{ inputType }: { inputType?: 'input' | 'autocomplete' | 'init' }
|
||||
) {
|
||||
setValue(value: string, { inputType }: { inputType?: 'input' | 'autocomplete' | 'init' }) {
|
||||
this.value = value;
|
||||
this.onChange(value);
|
||||
this.onTouched();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// start:ng42.barrel
|
||||
export * from './is-array';
|
||||
export * from './is-boolean';
|
||||
export * from './is-number';
|
||||
export * from './is-string';
|
||||
// end:ng42.barrel
|
||||
|
||||
3
apps/utils/common/src/lib/is-boolean.ts
Normal file
3
apps/utils/common/src/lib/is-boolean.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isBoolean(value: any) {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
368
package-lock.json
generated
368
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user