mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
120
angular.json
120
angular.json
@@ -2616,6 +2616,126 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ui/autocomplete": {
|
||||
"projectType": "library",
|
||||
"root": "apps/ui/autocomplete",
|
||||
"sourceRoot": "apps/ui/autocomplete/src",
|
||||
"prefix": "ui",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/ui/autocomplete/tsconfig.lib.json",
|
||||
"project": "apps/ui/autocomplete/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/ui/autocomplete/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "apps/ui/autocomplete/src/test.ts",
|
||||
"tsConfig": "apps/ui/autocomplete/tsconfig.spec.json",
|
||||
"karmaConfig": "apps/ui/autocomplete/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"apps/ui/autocomplete/tsconfig.lib.json",
|
||||
"apps/ui/autocomplete/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ui/switch": {
|
||||
"projectType": "library",
|
||||
"root": "apps/ui/switch",
|
||||
"sourceRoot": "apps/ui/switch/src",
|
||||
"prefix": "ui",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/ui/switch/tsconfig.lib.json",
|
||||
"project": "apps/ui/switch/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/ui/switch/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "apps/ui/switch/src/test.ts",
|
||||
"tsConfig": "apps/ui/switch/tsconfig.spec.json",
|
||||
"karmaConfig": "apps/ui/switch/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"apps/ui/switch/tsconfig.lib.json",
|
||||
"apps/ui/switch/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ui/tooltip": {
|
||||
"projectType": "library",
|
||||
"root": "apps/ui/tooltip",
|
||||
"sourceRoot": "apps/ui/tooltip/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/ui/tooltip/tsconfig.lib.json",
|
||||
"project": "apps/ui/tooltip/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/ui/tooltip/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "apps/ui/tooltip/src/test.ts",
|
||||
"tsConfig": "apps/ui/tooltip/tsconfig.spec.json",
|
||||
"karmaConfig": "apps/ui/tooltip/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"apps/ui/tooltip/tsconfig.lib.json",
|
||||
"apps/ui/tooltip/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "sales"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
*ngFor="let contributor of contributors$ | async; let last = last"
|
||||
class="autor"
|
||||
[routerLink]="['/product/search/results']"
|
||||
[queryParams]="{ query: contributor, inputSelector: 'author' }"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true)">
|
||||
<button class="filter" [class.active]="true" (click)="toggleFilterOverlay()">
|
||||
<ui-icon size="20px" icon="filter_alit"></ui-icon>
|
||||
<span class="label">Filter</span>
|
||||
</button>
|
||||
|
||||
<ng-container>
|
||||
<router-outlet></router-outlet>
|
||||
</ng-container>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div class="filter-overlay" [class.active]="filterActive$ | async">
|
||||
<div class="filter-content">
|
||||
<div class="filter-close-right">
|
||||
<button class="filter-close" (click)="filterActive$.next(false)">
|
||||
<ui-icon size="20px" icon="close"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="filter-header">
|
||||
Filter
|
||||
</h2>
|
||||
<page-article-search-filter
|
||||
(lastSelectedFilterCategory)="lastSelectedFilterCategory = $event"
|
||||
[updateFilterCategory]="lastSelectedFilterCategory"
|
||||
(exitFilter)="filterActive$.next(false)"
|
||||
*ngIf="filterActive$ | async"
|
||||
>
|
||||
</page-article-search-filter>
|
||||
</div>
|
||||
</div>
|
||||
<page-article-search-filter [@slideInOut] *ngIf="showFilterOverlay" (close)="toggleFilterOverlay()"></page-article-search-filter>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
:host {
|
||||
@apply flex flex-col w-full box-content absolute;
|
||||
}
|
||||
|
||||
.filter {
|
||||
@apply absolute font-sans flex items-center font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center;
|
||||
|
||||
right: 0;
|
||||
top: -52px;
|
||||
@apply absolute font-sans flex items-center font-bold bg-gray-400 border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center right-0 top-0;
|
||||
min-width: 106px;
|
||||
|
||||
.label {
|
||||
@@ -18,33 +11,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
@apply max-w-content mx-auto mt-px-25 px-px-15;
|
||||
|
||||
.filter-close-right {
|
||||
@apply pr-px-10 text-right;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
@apply text-center text-page-heading mt-0;
|
||||
}
|
||||
|
||||
button.filter-close {
|
||||
@apply border-0 bg-transparent text-ucla-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-overlay {
|
||||
@apply fixed bg-glitter z-fixed;
|
||||
|
||||
transform: translatex(100%);
|
||||
transition: transform 0.5s ease-out;
|
||||
top: 135px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&.active {
|
||||
transform: translatex(0%);
|
||||
}
|
||||
page-article-search-filter {
|
||||
@apply fixed right-0 bottom-0 left-0 z-fixed;
|
||||
top: 136px;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
|
||||
import { delay, map, switchMap } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from './article-search-new.store';
|
||||
import { UiFilterAutocompleteProvider, UiFilterScanProvider } from '@ui/filter';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ArticleSearchService } from './article-search.store';
|
||||
import { FocusSearchboxEvent } from './focus-searchbox.event';
|
||||
import { ArticleSearchMainAutocompleteProvider, ArticleSearchMainScanProviderService } from './providers';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search',
|
||||
templateUrl: 'article-search.component.html',
|
||||
styleUrls: ['article-search.component.scss'],
|
||||
providers: [ArticleSearchStore, FocusSearchboxEvent],
|
||||
providers: [
|
||||
FocusSearchboxEvent,
|
||||
ArticleSearchService,
|
||||
{
|
||||
provide: UiFilterAutocompleteProvider,
|
||||
useClass: ArticleSearchMainAutocompleteProvider,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: UiFilterScanProvider,
|
||||
useClass: ArticleSearchMainScanProviderService,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('slideInOut', [
|
||||
transition(':enter', [style({ transform: 'translateX(100%)' }), animate('250ms', style({ transform: 'translateX(0%)' }))]),
|
||||
transition(':leave', [style({ transform: '*' }), animate('250ms', style({ transform: 'translateX(100%)' }))]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
filterActive$ = new BehaviorSubject<boolean>(false);
|
||||
showMainContent$ = this.getShowMainContent();
|
||||
lastSelectedFilterCategory: Filter;
|
||||
showFilterOverlay = false;
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
hasFilter$: Observable<boolean> = this.store.queryParamsFilter$.pipe(map((filter) => filter !== ''));
|
||||
|
||||
constructor(
|
||||
private store: ArticleSearchStore,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private application: ApplicationService,
|
||||
private focusSearchbox: FocusSearchboxEvent
|
||||
private router: Router,
|
||||
private articleSearch: ArticleSearchService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -37,13 +54,19 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
this.filterActive$.pipe(delay(500)).subscribe((active) => {
|
||||
if (!active) {
|
||||
this.focusSearchbox.emit();
|
||||
this.articleSearch.searchCompleted.subscribe((s) => {
|
||||
if (s.searchState === '') {
|
||||
if (s.hits === 1) {
|
||||
const item = s.items.find((f) => f);
|
||||
this.router.navigate(['/product', 'details', item.id]);
|
||||
} else {
|
||||
const params = s.filter.getQueryParams();
|
||||
this.router.navigate(['/product', 'search', 'results'], {
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -61,15 +84,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
getShowMainContent(animationDelayInMs: number = 500): Observable<boolean> {
|
||||
return this.filterActive$.pipe(
|
||||
switchMap((filterActive) => {
|
||||
const onExitMainContent = filterActive;
|
||||
if (onExitMainContent) {
|
||||
return of(!filterActive).pipe(delay(animationDelayInMs));
|
||||
}
|
||||
return of(!filterActive);
|
||||
})
|
||||
);
|
||||
toggleFilterOverlay() {
|
||||
this.showFilterOverlay = !this.showFilterOverlay;
|
||||
}
|
||||
}
|
||||
|
||||
138
apps/page/catalog/src/lib/article-search/article-search.store.ts
Normal file
138
apps/page/catalog/src/lib/article-search/article-search.store.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
|
||||
export interface ArticleSearchState {
|
||||
processId: number;
|
||||
filter: UiFilter;
|
||||
searchState: '' | 'fetching' | 'empty' | 'error';
|
||||
items: ItemDTO[];
|
||||
hits: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
|
||||
get processId() {
|
||||
return this.get((s) => s.processId);
|
||||
}
|
||||
|
||||
filter$ = this.select((s) => s.filter);
|
||||
|
||||
get filter() {
|
||||
return this.get((s) => s.filter);
|
||||
}
|
||||
|
||||
items$ = this.select((s) => s.items);
|
||||
|
||||
get items() {
|
||||
return this.get((s) => s.items);
|
||||
}
|
||||
|
||||
get friendlyName() {
|
||||
return this.filter.input
|
||||
?.find((f) => f.group === 'main')
|
||||
.input?.find((f) => f)
|
||||
.toStringValue();
|
||||
}
|
||||
|
||||
fetching$ = this.select((s) => s.searchState === 'fetching');
|
||||
|
||||
searchStarted = new Subject<{ clear?: boolean; reload?: boolean }>();
|
||||
|
||||
searchCompleted = new Subject<ArticleSearchState>();
|
||||
|
||||
searchboxHint$ = this.select((s) => (s.searchState === 'empty' ? 'Keine Suchergebnisse' : undefined));
|
||||
|
||||
hits$ = this.select((s) => s.hits);
|
||||
|
||||
get hits() {
|
||||
return this.get((s) => s.hits);
|
||||
}
|
||||
|
||||
constructor(private catalog: DomainCatalogService) {
|
||||
super({
|
||||
filter: undefined,
|
||||
hits: 0,
|
||||
items: [],
|
||||
processId: 0,
|
||||
searchState: '',
|
||||
});
|
||||
this.setDefaultFilter();
|
||||
}
|
||||
|
||||
setProcess(processId: number) {
|
||||
this.patchState({ processId });
|
||||
}
|
||||
|
||||
setItems(items: ItemDTO[]) {
|
||||
this.patchState({ items });
|
||||
}
|
||||
|
||||
setHits(hits: number) {
|
||||
this.patchState({ hits });
|
||||
}
|
||||
|
||||
async setDefaultFilter() {
|
||||
const filter = await this.catalog
|
||||
.getSettings()
|
||||
.pipe(map((settings) => UiFilter.create(settings)))
|
||||
.toPromise();
|
||||
|
||||
this.setFilter(filter);
|
||||
}
|
||||
|
||||
setFilter(filter: UiFilter) {
|
||||
this.patchState({ filter });
|
||||
}
|
||||
|
||||
search = this.effect((options$: Observable<{ clear?: boolean; reload?: boolean }>) =>
|
||||
options$.pipe(
|
||||
tap((_) => this.patchState({ searchState: 'fetching' })),
|
||||
debounceTime(250),
|
||||
withLatestFrom(this.filter$, this.items$),
|
||||
switchMap(([options, filter, items]) =>
|
||||
this.catalog
|
||||
.search({
|
||||
queryToken: {
|
||||
...filter.getQueryToken(),
|
||||
skip: options.clear || options.reload ? 0 : items.length,
|
||||
take: options.reload ? items.length : 25,
|
||||
friendlyName: this.friendlyName,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
const searchState = res.hits ? '' : 'empty';
|
||||
if (options.clear || options.reload) {
|
||||
this.patchState({
|
||||
hits: res.hits,
|
||||
items: res.result,
|
||||
searchState,
|
||||
});
|
||||
} else {
|
||||
const items = this.get((s) => s.items);
|
||||
this.patchState({
|
||||
hits: res.hits,
|
||||
items: [...items, ...res.result],
|
||||
searchState,
|
||||
});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.patchState({ hits: 0, searchState: 'error', items: [] });
|
||||
}
|
||||
),
|
||||
tap((_) => {
|
||||
this.searchCompleted.next(this.get());
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiInput } from '@ui/filter';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ArticleSearchMainAutocompleteProvider extends UiFilterAutocompleteProvider {
|
||||
for = 'main';
|
||||
|
||||
constructor(private domainCatalogSearch: DomainCatalogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
complete(input: UiInput): Observable<UiFilterAutocomplete[]> {
|
||||
if (input.value?.length > 2) {
|
||||
return this.domainCatalogSearch
|
||||
.searchComplete({
|
||||
queryToken: {
|
||||
filter: input?.parent?.parent?.getQueryToken()?.filter,
|
||||
input: input.value,
|
||||
catalogType: undefined,
|
||||
take: 5,
|
||||
type: 'qs',
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
catchError(() => of({ result: [] })),
|
||||
map((res) => res.result)
|
||||
);
|
||||
}
|
||||
|
||||
return of([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UiFilterScanProvider } from '@ui/filter';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { NEVER, Observable, of } from 'rxjs';
|
||||
import { catchError, map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ArticleSearchMainScanProviderService extends UiFilterScanProvider {
|
||||
for = 'main';
|
||||
|
||||
constructor(private nativeContainer: NativeContainerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
return this.nativeContainer.openScanner('scanBook').pipe(
|
||||
mergeMap((result) => {
|
||||
if (result.status === 'RESULT') {
|
||||
return of(result.data);
|
||||
}
|
||||
return NEVER;
|
||||
}),
|
||||
catchError((err) => {
|
||||
return NEVER;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './article-search-main-autocomplete.provider';
|
||||
export * from './article-search-main-scan.provider';
|
||||
// end:ng42.barrel
|
||||
@@ -1,29 +1,22 @@
|
||||
<div class="filter-chips">
|
||||
<page-filter-chips class="filter" (filterChange)="checkMainFilter($event)" [filter]="mainFilters$ | async"></page-filter-chips>
|
||||
<page-filter-chips class="filter" (filterChange)="checkInputFilter($event)" [filter]="inputFilters$ | async"></page-filter-chips>
|
||||
</div>
|
||||
<ng-container *ngIf="filter$ | async; let filter">
|
||||
<div class="catalog-search-filter-content">
|
||||
<button class="btn-close" type="button" (click)="close.emit()">
|
||||
<ui-icon icon="close" size="20px"></ui-icon>
|
||||
</button>
|
||||
|
||||
<page-article-searchbox mode="filter" (closeFilterOverlay)="closeOverlayAfterSearch()"></page-article-searchbox>
|
||||
<div class="catalog-search-filter-content-main">
|
||||
<h1 class="title">Filter</h1>
|
||||
<ui-filter [filter]="filter" [loading]="fetching$ | async" (search)="applyFilter(filter)" [hint]="searchboxHint$ | async"></ui-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="filters$ | async; let selectedFilters">
|
||||
<ng-container *ngIf="initialFiltersBackend$ | async; let initialFilters">
|
||||
<ui-selected-filter-options [value]="selectedFilters" [initialFilter]="initialFilters" (valueChange)="updateFilter($event)">
|
||||
</ui-selected-filter-options>
|
||||
<div class="cta-wrapper">
|
||||
<button class="cta-reset-filter" (click)="resetFilter()">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
|
||||
<ui-filter-group
|
||||
theme="filter"
|
||||
[value]="selectedFilters"
|
||||
(activeChange)="updateCategory($event)"
|
||||
[active]="updateFilterCategory$ | async"
|
||||
(valueChange)="updateFilter($event)"
|
||||
></ui-filter-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="sticky-cta-wrapper">
|
||||
<button class="apply-filter" (click)="applyFilters()">
|
||||
<span>
|
||||
<button class="cta-apply-filter" (click)="applyFilter(filter)">
|
||||
Filter anwenden
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
:host {
|
||||
@apply flex flex-col box-border w-full;
|
||||
@apply block bg-glitter;
|
||||
}
|
||||
|
||||
.sticky-cta-wrapper {
|
||||
@apply fixed text-center inset-x-0 bottom-0;
|
||||
bottom: 30px;
|
||||
.catalog-search-filter-content {
|
||||
@apply relative mx-auto;
|
||||
max-width: 916px;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
@apply flex flex-row justify-center;
|
||||
.btn-close {
|
||||
@apply absolute text-cool-grey top-0 right-4 outline-none border-none bg-transparent;
|
||||
}
|
||||
|
||||
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;
|
||||
.catalog-search-filter-content-main {
|
||||
h1.title {
|
||||
@apply text-center;
|
||||
}
|
||||
}
|
||||
|
||||
button.apply-filter:disabled {
|
||||
@apply bg-inactive-customer;
|
||||
::ng-deep ui-filter-input-options .input-options {
|
||||
max-height: calc(100vh - 555px);
|
||||
}
|
||||
|
||||
button.apply-filter.loading {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 17px;
|
||||
.cta-wrapper {
|
||||
@apply fixed bottom-8 whitespace-nowrap;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
ui-selected-filter-options {
|
||||
@apply my-px-8;
|
||||
.cta-reset-filter,
|
||||
.cta-apply-filter {
|
||||
@apply text-lg font-bold px-6 py-3 rounded-full border-solid border-2 border-brand outline-none mx-2;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply inline-flex;
|
||||
.cta-reset-filter {
|
||||
@apply bg-white text-brand;
|
||||
}
|
||||
|
||||
.spin {
|
||||
@apply animate-spin;
|
||||
.cta-apply-filter {
|
||||
@apply text-white bg-brand;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { clone, cloneDeep } from 'lodash';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
import { FocusSearchboxEvent } from '../focus-searchbox.event';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search-filter',
|
||||
@@ -12,101 +10,47 @@ import { FocusSearchboxEvent } from '../focus-searchbox.event';
|
||||
styleUrls: ['search-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
@Output() exitFilter = new EventEmitter<void>();
|
||||
@Output() lastSelectedFilterCategory = new EventEmitter<Filter>();
|
||||
export class ArticleSearchFilterComponent implements OnInit {
|
||||
@Output()
|
||||
close = new EventEmitter();
|
||||
|
||||
readonly initialFiltersBackend$ = this.store.defaultFilter$.pipe(map((filter) => filter.filter));
|
||||
fetching$: Observable<boolean>;
|
||||
|
||||
readonly filters$ = this.store.filter$;
|
||||
readonly inputFilters$ = this.store.inputSelectorFilter$;
|
||||
readonly mainFilters$ = this.store.mainFilter$;
|
||||
filter$: Observable<UiFilter>;
|
||||
|
||||
queryCopy: string;
|
||||
filtersCopy: Filter[];
|
||||
inputFiltersCopy: Filter[];
|
||||
mainFiltersCopy: Filter[];
|
||||
isFilterApplied = false;
|
||||
searchboxHint$ = this.articleSearch.searchboxHint$;
|
||||
|
||||
readonly searchState$ = this.store.searchState$;
|
||||
|
||||
/* @internal */
|
||||
updateFilterCategory$ = new BehaviorSubject<Filter>(undefined);
|
||||
|
||||
@Input()
|
||||
get updateFilterCategory(): Filter {
|
||||
return this.updateFilterCategory$.value;
|
||||
}
|
||||
|
||||
set updateFilterCategory(value) {
|
||||
this.updateFilterCategory$.next(value);
|
||||
}
|
||||
|
||||
searchStateSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private store: ArticleSearchStore, private focusSearchbox: FocusSearchboxEvent) {}
|
||||
constructor(private articleSearch: ArticleSearchService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.copyFilterLatestStatus();
|
||||
this.fetching$ = this.articleSearch.fetching$;
|
||||
this.filter$ = this.articleSearch.filter$.pipe(
|
||||
map((filter) => UiFilter.create(filter))
|
||||
// tap((filter) =>
|
||||
// filter.fromQueryParams({
|
||||
// main_qs: 'harry potter',
|
||||
// filter_format: 'eb;!hc',
|
||||
// filter_dbhwgr: '110;121',
|
||||
// main_author: 'author',
|
||||
// filter_region: '9780*|9781*;97884*',
|
||||
// 'filter_reading-age': '1-10',
|
||||
// main_stock: '1-',
|
||||
// })
|
||||
// )
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (!this.isFilterApplied) {
|
||||
this.resetFilterToLatestStatus();
|
||||
}
|
||||
|
||||
if (!!this.searchStateSubscription) {
|
||||
this.searchStateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
closeOverlayAfterSearch() {
|
||||
this.isFilterApplied = true;
|
||||
this.exitFilter.emit();
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
this.isFilterApplied = true;
|
||||
this.store.search({ clear: true });
|
||||
|
||||
this.searchStateSubscription = this.store.searchState$.subscribe((state) => {
|
||||
if (state !== 'fetching' && state !== 'empty') {
|
||||
this.closeOverlayAfterSearch();
|
||||
applyFilter(value: UiFilter) {
|
||||
this.articleSearch.setFilter(value);
|
||||
this.articleSearch.search({ clear: true });
|
||||
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => {
|
||||
if (s.searchState === '') {
|
||||
this.close.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkInputFilter(filter: Filter[]) {
|
||||
this.store.setInputSelectorFilter({ filter });
|
||||
this.focusSearchbox.emit();
|
||||
}
|
||||
|
||||
checkMainFilter(filter: Filter[]) {
|
||||
this.store.setMainFilter({ filter });
|
||||
this.focusSearchbox.emit();
|
||||
}
|
||||
|
||||
updateCategory(filter: Filter) {
|
||||
this.lastSelectedFilterCategory.emit(filter);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
updateFilter(filter: Filter[]) {
|
||||
this.store.setFilter({ filter });
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
async copyFilterLatestStatus() {
|
||||
this.queryCopy = clone(this.store.query);
|
||||
this.filtersCopy = cloneDeep(await this.filters$.pipe(first()).toPromise());
|
||||
this.inputFiltersCopy = cloneDeep(await this.inputFilters$.pipe(first()).toPromise());
|
||||
this.mainFiltersCopy = cloneDeep(await this.mainFilters$.pipe(first()).toPromise());
|
||||
}
|
||||
|
||||
resetFilterToLatestStatus() {
|
||||
this.store.setQuery({ query: this.queryCopy });
|
||||
this.store.setFilter({ filter: this.filtersCopy });
|
||||
this.store.setInputSelectorFilter({ filter: this.inputFiltersCopy });
|
||||
this.store.setMainFilter({ filter: this.mainFiltersCopy });
|
||||
resetFilter() {
|
||||
this.articleSearch.setDefaultFilter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiFilterModule } from '@ui/filter';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ArticleSearchboxModule } from '../containers/article-searchbox/article-searchbox.module';
|
||||
import { FilterChipsModule } from '../containers/filter-chips/filter-chips.module';
|
||||
|
||||
import { ArticleSearchFilterComponent } from './search-filter.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FilterChipsModule, ArticleSearchboxModule, UiIconModule, UiFilterModule],
|
||||
imports: [CommonModule, UiFilterNextModule, UiIconModule],
|
||||
exports: [ArticleSearchFilterComponent],
|
||||
declarations: [ArticleSearchFilterComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
<p class="info">
|
||||
Welchen Artikel suchen Sie?
|
||||
</p>
|
||||
<div class="filter-chips">
|
||||
<page-filter-chips (filterChange)="checkMainFilter($event)" [filter]="mainFilter$ | async"></page-filter-chips>
|
||||
<page-filter-chips (filterChange)="checkInputFilter($event)" [filter]="inputFilter$ | async"></page-filter-chips>
|
||||
</div>
|
||||
<page-article-searchbox mode="main"></page-article-searchbox>
|
||||
<div class="recent-searches-wrapper">
|
||||
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3>
|
||||
<ul>
|
||||
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async">
|
||||
<button (click)="setQueryHistory(recentQuery.friendlyName)">
|
||||
<ui-icon icon="search" size="15px"></ui-icon>
|
||||
<p>{{ recentQuery.friendlyName }}</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ng-container *ngIf="filter$ | async; let filter">
|
||||
<ui-filter-filter-group-main [inputGroup]="filter?.filter | group: 'main'"></ui-filter-filter-group-main>
|
||||
<ui-filter-input-group-main
|
||||
[hint]="searchboxHint$ | async"
|
||||
[loading]="fetching$ | async"
|
||||
[inputGroup]="filter?.input | group: 'main'"
|
||||
(search)="search(filter)"
|
||||
[showDescription]="false"
|
||||
></ui-filter-input-group-main>
|
||||
|
||||
<div class="recent-searches-wrapper">
|
||||
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3>
|
||||
<ul>
|
||||
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async">
|
||||
<button (click)="setQueryHistory(filter, recentQuery.friendlyName)">
|
||||
<ui-icon icon="search" size="15px"></ui-icon>
|
||||
<p>{{ recentQuery.friendlyName }}</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,19 @@
|
||||
height: calc(100vh - 380px);
|
||||
}
|
||||
|
||||
ui-filter-filter-group-main {
|
||||
@apply mb-8 w-full;
|
||||
}
|
||||
|
||||
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
|
||||
@apply bg-glitter !important text-ucla-blue !important;
|
||||
}
|
||||
|
||||
ui-filter-input-group-main {
|
||||
@apply block mx-auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.recent-searches-wrapper {
|
||||
@apply flex flex-col mx-auto items-start py-6;
|
||||
width: 50%;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { combineLatest, NEVER, Subscription } from 'rxjs';
|
||||
import { catchError, debounceTime, first, switchMap } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
import { isEqual } from 'lodash';
|
||||
import { catchError, debounceTime, first } from 'rxjs/operators';
|
||||
import { FocusSearchboxEvent } from '../focus-searchbox.event';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { isEmpty, isEqual } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search-main',
|
||||
@@ -18,18 +18,22 @@ import { FocusSearchboxEvent } from '../focus-searchbox.event';
|
||||
})
|
||||
export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
readonly history$ = this.catalog.getSearchHistory({ take: 5 }).pipe(catchError(() => NEVER));
|
||||
readonly inputFilter$ = this.store.inputSelectorFilter$;
|
||||
readonly mainFilter$ = this.store.mainFilter$;
|
||||
|
||||
fetching$ = this.searchService.fetching$;
|
||||
|
||||
filter$ = this.searchService.filter$;
|
||||
|
||||
searchboxHint$ = this.searchService.searchboxHint$;
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
constructor(
|
||||
private store: ArticleSearchStore,
|
||||
private searchService: ArticleSearchService,
|
||||
private catalog: DomainCatalogService,
|
||||
private route: ActivatedRoute,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private application: ApplicationService,
|
||||
private focusSearchbox: FocusSearchboxEvent
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -37,42 +41,80 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
|
||||
.pipe(debounceTime(0))
|
||||
.subscribe(async ([processId, queryParams]) => {
|
||||
// Setzen des aktuellen Prozesses
|
||||
if (this.store?.processId !== processId) {
|
||||
this.store.patchState({ processId });
|
||||
if (!(this.searchService.filter instanceof UiFilter)) {
|
||||
await this.searchService.setDefaultFilter();
|
||||
}
|
||||
|
||||
// Updaten der QueryParams wenn diese sich ändern
|
||||
if (!isEqual(this.store.queryParams, queryParams)) {
|
||||
const params = { ...queryParams };
|
||||
delete params.scrollPos;
|
||||
this.store.setQueryParams({ params });
|
||||
if (processId !== this.searchService.processId) {
|
||||
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
|
||||
this.searchService.setProcess(processId);
|
||||
}
|
||||
|
||||
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'main']).pipe(first()).toPromise();
|
||||
const cleanQueryParams = this.cleanupQueryParams(queryParams);
|
||||
|
||||
for (const crumb of crumbs) {
|
||||
this.breadcrumb.removeBreadcrumbsAfter(crumb.id, ['catalog']);
|
||||
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
|
||||
if (isEmpty(cleanQueryParams)) {
|
||||
await this.searchService.setDefaultFilter();
|
||||
}
|
||||
|
||||
this.searchService.filter.fromQueryParams(queryParams);
|
||||
}
|
||||
|
||||
this.removeResultsAndDetailsBreadcrumbs(processId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.unsubscribe();
|
||||
|
||||
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
}
|
||||
|
||||
checkInputFilter(filter: Filter[]) {
|
||||
this.store.setInputSelectorFilter({ filter });
|
||||
this.focusSearchbox.emit();
|
||||
search(filter: UiFilter) {
|
||||
this.searchService.setFilter(filter);
|
||||
this.searchService.search({ clear: true });
|
||||
}
|
||||
|
||||
checkMainFilter(filter: Filter[]) {
|
||||
this.store.setMainFilter({ filter });
|
||||
this.focusSearchbox.emit();
|
||||
setQueryHistory(filter: UiFilter, query: string) {
|
||||
filter.fromQueryParams({ main_qs: query });
|
||||
}
|
||||
|
||||
setQueryHistory(recentQuery: string) {
|
||||
this.store.setQuery({ query: recentQuery });
|
||||
cleanupQueryParams(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (Object.prototype.hasOwnProperty.call(clean, key)) {
|
||||
if (clean[key] == undefined) {
|
||||
delete clean[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
async updateBreadcrumb(processId: number, queryParams: Record<string, string>) {
|
||||
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'main']).pipe(first()).toPromise();
|
||||
|
||||
crumbs.forEach((crumb) => {
|
||||
this.breadcrumb.patchBreadcrumb(crumb.id, {
|
||||
params: queryParams,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async removeResultsAndDetailsBreadcrumbs(processId: number) {
|
||||
const resultsCrumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'results']).pipe(first()).toPromise();
|
||||
const detailCrumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'details']).pipe(first()).toPromise();
|
||||
|
||||
resultsCrumbs?.forEach((crumb) => {
|
||||
this.breadcrumb.removeBreadcrumb(crumb.id);
|
||||
});
|
||||
|
||||
detailCrumbs?.forEach((crumb) => {
|
||||
this.breadcrumb.removeBreadcrumb(crumb.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ArticleSearchboxModule } from '../containers/article-searchbox/article-searchbox.module';
|
||||
import { FilterChipsModule } from '../containers/filter-chips/filter-chips.module';
|
||||
|
||||
import { ArticleSearchMainComponent } from './search-main.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule, FilterChipsModule, ArticleSearchboxModule],
|
||||
imports: [CommonModule, UiIconModule, UiFilterNextModule],
|
||||
exports: [ArticleSearchMainComponent],
|
||||
declarations: [ArticleSearchMainComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<div class="grw"></div>
|
||||
<div class="order-by-filter-button-wrapper">
|
||||
<button class="order-by-filter-button" type="button" *ngFor="let by of orderBy$ | async" (click)="setActive(by.next)">
|
||||
<button class="order-by-filter-button" type="button" *ngFor="let by of orderByKeys" (click)="setActive(by)">
|
||||
<span>
|
||||
{{ by.by }}
|
||||
{{ by }}
|
||||
</span>
|
||||
<ui-icon [class.asc]="by.active && !by.desc" [class.desc]="by.active && by.desc" icon="arrow" size="14px"></ui-icon>
|
||||
<ui-icon
|
||||
[class.asc]="by === activeOrderBy?.by && !activeOrderBy.desc"
|
||||
[class.desc]="by === activeOrderBy?.by && activeOrderBy.desc"
|
||||
icon="arrow"
|
||||
size="14px"
|
||||
></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grw"></div>
|
||||
<div *ngIf="hits$ | async; let hits" class="hits">{{ hits }} Titel</div>
|
||||
<div *ngIf="hits" class="hits">{{ hits }} Titel</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
:host {
|
||||
@apply box-border flex justify-center items-center;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.order-by-filter-button-wrapper {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { OrderByDTO } from '@swagger/cat';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../../article-search-new.store';
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, Output, EventEmitter } from '@angular/core';
|
||||
import { UiOrderBy } from '@ui/filter';
|
||||
|
||||
@Component({
|
||||
selector: 'page-order-by-filter',
|
||||
@@ -11,38 +8,43 @@ import { ArticleSearchStore } from '../../article-search-new.store';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OrderByFilterComponent {
|
||||
active$ = this.store.orderBy$;
|
||||
hits$ = this.store.hits$;
|
||||
@Input()
|
||||
orderBy: UiOrderBy[];
|
||||
|
||||
orderBy$ = combineLatest([this.store.orderByOptions$.pipe(map((bys) => bys.map((b) => ({ ...b, desc: !!b.desc })))), this.active$]).pipe(
|
||||
map(([orderBy, activeOrderBy]) => {
|
||||
const bys = orderBy.map((o) => o.by).reduce((agg, by) => (!agg.includes(by) ? [...agg, by] : agg), []);
|
||||
@Input()
|
||||
hits: number;
|
||||
|
||||
const orderBys: (OrderByDTO & { active: boolean; next?: OrderByDTO })[] = [];
|
||||
@Output()
|
||||
selectedOrderByChange = new EventEmitter<UiOrderBy>();
|
||||
|
||||
for (const by of bys) {
|
||||
const asc = orderBy.find((ob) => ob.by === by && !ob.desc);
|
||||
const desc = orderBy.find((ob) => ob.by === by && !!ob.desc);
|
||||
const ascActive = asc && asc.by === activeOrderBy?.by && !activeOrderBy?.desc;
|
||||
const descActive = desc && desc.by === activeOrderBy?.by && activeOrderBy?.desc;
|
||||
get orderByKeys() {
|
||||
return this.orderBy?.map((ob) => ob.by).filter((key, idx, self) => self.indexOf(key) === idx);
|
||||
}
|
||||
|
||||
if (!!desc && descActive) {
|
||||
orderBys.push({ ...desc, active: true, next: asc });
|
||||
} else if (!!asc && ascActive) {
|
||||
orderBys.push({ ...asc, active: true });
|
||||
} else if (!!asc || !!desc) {
|
||||
orderBys.push({ ...desc, active: false, next: desc || asc });
|
||||
}
|
||||
get activeOrderBy() {
|
||||
return this.orderBy?.find((f) => f.selected);
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
setActive(orderBy: string) {
|
||||
const active = this.activeOrderBy;
|
||||
const orderBys = this.orderBy?.filter((f) => f.by === orderBy);
|
||||
let next: UiOrderBy;
|
||||
|
||||
if (orderBys?.length) {
|
||||
if (active?.by !== orderBy) {
|
||||
next = orderBys?.find((f) => !f.desc);
|
||||
} else if (!active.desc) {
|
||||
next = orderBys?.find((f) => f.desc);
|
||||
}
|
||||
}
|
||||
|
||||
return orderBys;
|
||||
})
|
||||
);
|
||||
this.orderBy?.filter((f) => f.selected)?.forEach((f) => f.setSelected(false));
|
||||
|
||||
constructor(private store: ArticleSearchStore) {}
|
||||
next?.setSelected(true);
|
||||
|
||||
setActive(orderBy: OrderByDTO) {
|
||||
this.store.setOrderBy({ orderBy });
|
||||
this.store.search({ reload: true });
|
||||
this.selectedOrderByChange.next(next);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<div class="item-contributors">
|
||||
<a
|
||||
*ngFor="let contributor of contributors; let last = last"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ query: contributor, inputSelector: 'author' }"
|
||||
(click)="$event.stopPropagation()"
|
||||
[routerLink]="['/product/search/results']"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<page-order-by-filter></page-order-by-filter>
|
||||
<page-order-by-filter
|
||||
[hits]="hits$ | async"
|
||||
[orderBy]="(filter$ | async)?.orderBy"
|
||||
(selectedOrderByChange)="search(); updateBreadcrumbs()"
|
||||
></page-order-by-filter>
|
||||
|
||||
<cdk-virtual-scroll-viewport
|
||||
#scrollContainer
|
||||
class="product-list scroll-bar"
|
||||
|
||||
@@ -4,11 +4,12 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { CacheService } from 'apps/core/cache/src/public-api';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
import { debounceTime, first } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-search-results',
|
||||
@@ -22,15 +23,18 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
loading$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
results$ = this.store.items$;
|
||||
fetching$ = this.store.searchState$.pipe(map((state) => state === 'fetching'));
|
||||
results$ = this.searchService.items$;
|
||||
fetching$ = this.searchService.fetching$;
|
||||
hits$ = this.searchService.hits$;
|
||||
|
||||
filter$ = this.searchService.filter$;
|
||||
|
||||
trackByItemId = (item: ItemDTO) => item.id;
|
||||
|
||||
private subscriptions = new Subscription();
|
||||
|
||||
constructor(
|
||||
private store: ArticleSearchStore,
|
||||
private searchService: ArticleSearchService,
|
||||
private route: ActivatedRoute,
|
||||
private application: ApplicationService,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
@@ -42,42 +46,36 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
|
||||
.pipe(debounceTime(0))
|
||||
.subscribe(async ([processId, queryParams]) => {
|
||||
// Wenn ein Prozess bereits zugewiesen ist und der Prozess sich ändert
|
||||
// Speicher Ergebnisse in den Cache und Update Breadcrumb Params
|
||||
if (this.store?.processId !== processId) {
|
||||
this.cacheCurrentItems();
|
||||
await this.updateBreadcrumbs();
|
||||
if (processId !== this.searchService.processId) {
|
||||
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) {
|
||||
this.cacheCurrentData(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
}
|
||||
this.searchService.setProcess(processId);
|
||||
}
|
||||
|
||||
// Setzen des aktuellen Prozesses
|
||||
this.store.patchState({ processId });
|
||||
|
||||
// Updaten der QueryParams wenn diese sich ändern
|
||||
// scrollPos muss entfernt werden um die items anhand der QueryParams zu cachen
|
||||
if (!isEqual(this.store.queryParams, queryParams)) {
|
||||
const params = { ...queryParams };
|
||||
delete params.scrollPos;
|
||||
const items = this.cache.get<ItemDTO[]>(params);
|
||||
this.store.setQueryParams({ params });
|
||||
this.store.setItems({ items });
|
||||
|
||||
this.store.search({ reload: true });
|
||||
if (!(this.searchService.filter instanceof UiFilter)) {
|
||||
await this.searchService.setDefaultFilter();
|
||||
}
|
||||
|
||||
// Nach dem setzen der Items im store an die letzte Position scrollen
|
||||
this.scrollTop(Number(queryParams.scrollPos ?? 0));
|
||||
const cleanQueryParams = this.cleanupQueryParams(queryParams);
|
||||
|
||||
// Fügt Breadcrumb hinzu falls dieser noch nicht vorhanden ist
|
||||
await this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name: `${this.store.query ? this.store.query : 'Alle Artikel'}`,
|
||||
path: '/product/search/results',
|
||||
params: queryParams,
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
});
|
||||
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
|
||||
this.searchService.filter.fromQueryParams(queryParams);
|
||||
const data = this.getCachedData(processId, queryParams);
|
||||
this.searchService.setItems(data.items);
|
||||
this.searchService.setHits(data.hits);
|
||||
|
||||
await this.removeDetailBreadcrumb(processId);
|
||||
await this.store.updateBreadcrumbs();
|
||||
if (data.items?.length === 0) {
|
||||
this.search();
|
||||
} else {
|
||||
this.scrollTop(Number(queryParams.scroll_position ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateBreadcrumbs(processId, queryParams);
|
||||
await this.createBreadcrumb(processId, queryParams);
|
||||
await this.removeDetailsBreadcrumb(processId);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -86,8 +84,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
this.loading$.complete();
|
||||
this.subscriptions?.unsubscribe();
|
||||
|
||||
this.cacheCurrentItems();
|
||||
this.updateBreadcrumbs();
|
||||
this.cacheCurrentData(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
}
|
||||
|
||||
search() {
|
||||
this.searchService.search({ clear: true });
|
||||
}
|
||||
|
||||
async removeDetailBreadcrumb(processId: number) {
|
||||
@@ -105,25 +107,85 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
async scrolledIndexChange(index: number) {
|
||||
const results = await this.results$.pipe(first()).toPromise();
|
||||
if (index >= results.length - 20 && results.length - 20 > 0) {
|
||||
this.store.search({ clear: false });
|
||||
this.searchService.search({ clear: false });
|
||||
}
|
||||
}
|
||||
|
||||
async updateBreadcrumbs() {
|
||||
const scrollPos = this.scrollContainer.measureScrollOffset('top');
|
||||
const processId = this.store.processId;
|
||||
const queryParams = { ...this.store.queryParams };
|
||||
async updateBreadcrumbs(
|
||||
processId: number = this.searchService.processId,
|
||||
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams()
|
||||
) {
|
||||
const scroll_position = this.scrollContainer.measureScrollOffset('top');
|
||||
|
||||
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results']).pipe(first()).toPromise();
|
||||
if (queryParams) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results'])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
const params = { ...queryParams, scrollPos };
|
||||
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
|
||||
const params = { ...queryParams, scroll_position };
|
||||
|
||||
for (const crumb of crumbs) {
|
||||
this.breadcrumb.patchBreadcrumb(crumb.id, { params });
|
||||
for (const crumb of crumbs) {
|
||||
this.breadcrumb.patchBreadcrumb(crumb.id, {
|
||||
name,
|
||||
params,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cacheCurrentItems() {
|
||||
this.cache.set(this.store.queryParams, this.store.items);
|
||||
async createBreadcrumb(processId: number, queryParams: Record<string, string>) {
|
||||
if (queryParams) {
|
||||
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
|
||||
await this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name,
|
||||
path: '/product/search/results',
|
||||
params: queryParams,
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async removeDetailsBreadcrumb(processId: number) {
|
||||
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'details']).pipe(first()).toPromise();
|
||||
|
||||
crumbs?.forEach((crumb) => this.breadcrumb.removeBreadcrumb(crumb.id));
|
||||
}
|
||||
|
||||
cacheCurrentData(processId: number, params: Record<string, string> = {}) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId) });
|
||||
|
||||
this.cache.set(qparams, {
|
||||
items: this.searchService.items,
|
||||
hits: this.searchService.hits,
|
||||
});
|
||||
}
|
||||
|
||||
getCachedData(processId: number, params: Record<string, string> = {}) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId) });
|
||||
|
||||
return (
|
||||
this.cache.get<{
|
||||
items: ItemDTO[];
|
||||
hits: number;
|
||||
}>(qparams) || { items: [], hits: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
cleanupQueryParams(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
delete clean['scroll_position'];
|
||||
|
||||
for (const key in clean) {
|
||||
if (Object.prototype.hasOwnProperty.call(clean, key)) {
|
||||
if (clean[key] == undefined) {
|
||||
delete clean[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const errorWhiteList: { url: string; codes: number[] }[] = [
|
||||
{ url: '/eiswebapi/v1', codes: [-1] },
|
||||
{ url: 'isa/logging', codes: [-1] },
|
||||
{ url: '/isa/userstate', codes: [404] },
|
||||
{ url: '/s/complete', codes: [400] },
|
||||
{
|
||||
url: '/availability/',
|
||||
codes: [401, 403, 404, 406, 407, 410, 412, 416, 418, 451, 500, 501, 502, 503, 504, 0, -1],
|
||||
|
||||
25
apps/ui/autocomplete/README.md
Normal file
25
apps/ui/autocomplete/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Autocomplete
|
||||
|
||||
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.2.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name --project autocomplete` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project autocomplete`.
|
||||
|
||||
> Note: Don't forget to add `--project autocomplete` or else it will be added to the default project in your `angular.json` file.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build autocomplete` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Publishing
|
||||
|
||||
After building your library with `ng build autocomplete`, go to the dist folder `cd dist/autocomplete` and run `npm publish`.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test autocomplete` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
32
apps/ui/autocomplete/karma.conf.js
Normal file
32
apps/ui/autocomplete/karma.conf.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
],
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, '../../../coverage/ui/autocomplete'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true,
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
7
apps/ui/autocomplete/ng-package.json
Normal file
7
apps/ui/autocomplete/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../../dist/ui/autocomplete",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
11
apps/ui/autocomplete/package.json
Normal file
11
apps/ui/autocomplete/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@ui/autocomplete",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^10.1.2",
|
||||
"@angular/core": "^10.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-content></ng-content>
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator';
|
||||
import { UiAutocompleteItemComponent } from './autocomplete-item.component';
|
||||
|
||||
describe('UiSearchboxAutocompleteItem', () => {
|
||||
let spectator: SpectatorHost<UiAutocompleteItemComponent>;
|
||||
const createComponent = createHostFactory(UiAutocompleteItemComponent);
|
||||
|
||||
beforeEach(() => (spectator = createComponent(`<button [uiAutocompleteItem]="item">My Autocomplete Item</button>`)));
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set the class ui-autocomplete-item', () => {
|
||||
expect(spectator.element).toHaveClass('ui-autocomplete-item');
|
||||
});
|
||||
|
||||
it('should not have class active if isActive is false', () => {
|
||||
spectator.component.isActive = false;
|
||||
spectator.detectChanges();
|
||||
expect(spectator.element).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should have class active if isActive is true', () => {
|
||||
spectator.component.isActive = true;
|
||||
spectator.detectChanges();
|
||||
expect(spectator.element).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should not be disabled if disabled is false', () => {
|
||||
spectator.setInput({ disabled: false });
|
||||
spectator.detectChanges();
|
||||
expect(spectator.element).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be disabled if disabled is true', () => {
|
||||
spectator.setInput({ disabled: true });
|
||||
spectator.detectChanges();
|
||||
expect(spectator.element).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display the content', () => {
|
||||
expect(spectator.element).toHaveText('My Autocomplete Item');
|
||||
});
|
||||
|
||||
it('should call handleClickEvent on click', () => {
|
||||
spyOn(spectator.component, 'handleClickEvent');
|
||||
spectator.click();
|
||||
expect(spectator.component.handleClickEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('setActiveStyles', () => {
|
||||
it('should set isActive to true', () => {
|
||||
spectator.component.setActiveStyles();
|
||||
expect(spectator.component.isActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInactiveStyles', () => {
|
||||
it('should set isActive to false', () => {
|
||||
spectator.component.setInactiveStyles();
|
||||
expect(spectator.component.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerOnClick()', () => {
|
||||
it('should set onClick', () => {
|
||||
const fn = (item: any) => item?.data;
|
||||
spectator.component.registerOnClick(fn);
|
||||
expect(spectator.component.onClick).toBe(fn);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleClickEvent()', () => {
|
||||
it('should call onClick with the item', () => {
|
||||
const item = { unit: 'test' };
|
||||
spectator.setInput({ item });
|
||||
spectator.detectComponentChanges();
|
||||
spyOn(spectator.component, 'onClick');
|
||||
|
||||
spectator.component.handleClickEvent();
|
||||
expect(spectator.component.onClick).toHaveBeenCalledWith(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
apps/ui/autocomplete/src/lib/autocomplete-item.component.ts
Normal file
52
apps/ui/autocomplete/src/lib/autocomplete-item.component.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Highlightable, ListKeyManagerOption } from '@angular/cdk/a11y';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: '[uiAutocompleteItem]',
|
||||
templateUrl: 'autocomplete-item.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class UiAutocompleteItemComponent implements Highlightable {
|
||||
@HostBinding('class.ui-autocomplete-item') clazz = true;
|
||||
|
||||
@Input('uiAutocompleteItem')
|
||||
item: any;
|
||||
|
||||
@HostBinding('class.active')
|
||||
isActive = false;
|
||||
|
||||
@Input()
|
||||
@HostBinding('disabled')
|
||||
disabled = false;
|
||||
|
||||
onClick = (item: any) => {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
@HostListener('click')
|
||||
handleClickEvent() {
|
||||
this.onClick?.call(null, this.item);
|
||||
}
|
||||
|
||||
registerOnClick(fn: (item: any) => void) {
|
||||
this.onClick = fn;
|
||||
}
|
||||
|
||||
setActiveStyles(): void {
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
setInactiveStyles(): void {
|
||||
this.isActive = false;
|
||||
}
|
||||
}
|
||||
3
apps/ui/autocomplete/src/lib/autocomplete.component.html
Normal file
3
apps/ui/autocomplete/src/lib/autocomplete.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="ui-autocomplete-output-wrapper" *ngIf="opend">
|
||||
<ng-content select="[uiAutocompleteItem]"></ng-content>
|
||||
</div>
|
||||
139
apps/ui/autocomplete/src/lib/autocomplete.component.spec.ts
Normal file
139
apps/ui/autocomplete/src/lib/autocomplete.component.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
|
||||
import { fakeAsync } from '@angular/core/testing';
|
||||
import { createHostFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiAutocompleteItemComponent } from './autocomplete-item.component';
|
||||
import { UiAutocompleteComponent } from './autocomplete.component';
|
||||
|
||||
describe('UiSearchboxAutocomplete', () => {
|
||||
let spectator: Spectator<UiAutocompleteComponent>;
|
||||
const createComponent = createHostFactory({
|
||||
component: UiAutocompleteComponent,
|
||||
declarations: [UiAutocompleteItemComponent],
|
||||
});
|
||||
|
||||
beforeEach(
|
||||
() =>
|
||||
(spectator = createComponent(`
|
||||
<ui-autocomplete>
|
||||
<button [uiAutocompleteItem]>My Autocomplete Item</button>
|
||||
</ui-autocomplete>`))
|
||||
);
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('ngAfterContentInit', () => {
|
||||
it('should create an instance of ActiveDescendantKeyManager and set it to listKeyManager', () => {
|
||||
spyOn(spectator.component, 'registerItemOnClick');
|
||||
spyOn(spectator.component.subscriptions, 'add');
|
||||
spectator.component.ngAfterContentInit();
|
||||
expect(spectator.component.listKeyManager instanceof ActiveDescendantKeyManager).toBe(true);
|
||||
expect(spectator.component.registerItemOnClick).toHaveBeenCalledTimes(1);
|
||||
expect(spectator.component.subscriptions.add).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnDestroy', () => {
|
||||
it('should call unsubscribe', () => {
|
||||
spyOn(spectator.component.subscriptions, 'unsubscribe');
|
||||
spectator.component.ngOnDestroy();
|
||||
expect(spectator.component.subscriptions.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleKeyboardEvent', () => {
|
||||
it('should emit selectItem and call close and not call listKeyManager.onKeyDown on Enter key', () => {
|
||||
spyOn(spectator.component, 'close');
|
||||
spyOn(spectator.component.listKeyManager, 'onKeydown');
|
||||
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test' });
|
||||
|
||||
let selectItem;
|
||||
spectator.output('selectItem').subscribe((item) => (selectItem = item));
|
||||
|
||||
spectator.component.opend = true;
|
||||
const event = new KeyboardEvent('keyup', { key: 'Enter' });
|
||||
spectator.component.handleKeyboardEvent(event);
|
||||
|
||||
expect(spectator.component.close).toHaveBeenCalled();
|
||||
expect(selectItem).toEqual({ item: 'Test' });
|
||||
expect(spectator.component.listKeyManager.onKeydown).not.toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should call close and not emit selectItem and not call listKeyManager.onKeydown on Escape key', () => {
|
||||
spyOn(spectator.component, 'close');
|
||||
spyOn(spectator.component.listKeyManager, 'onKeydown');
|
||||
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test' });
|
||||
|
||||
let selectItem;
|
||||
spectator.output('selectItem').subscribe((item) => (selectItem = item));
|
||||
|
||||
spectator.component.opend = true;
|
||||
const event = new KeyboardEvent('keyup', { key: 'Escape' });
|
||||
spectator.component.handleKeyboardEvent(event);
|
||||
|
||||
expect(spectator.component.close).toHaveBeenCalled();
|
||||
expect(selectItem).toBeUndefined();
|
||||
expect(spectator.component.listKeyManager.onKeydown).not.toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should call listKeyManager.onKeyDown and not emit selectItem and not call close on ArrowDown key', () => {
|
||||
spyOn(spectator.component, 'close');
|
||||
spyOn(spectator.component.listKeyManager, 'onKeydown');
|
||||
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test' });
|
||||
|
||||
let selectItem;
|
||||
spectator.output('selectItem').subscribe((item) => (selectItem = item));
|
||||
|
||||
spectator.component.opend = true;
|
||||
const event = new KeyboardEvent('keyup', { key: 'ArrowDown' });
|
||||
spectator.component.handleKeyboardEvent(event);
|
||||
|
||||
expect(spectator.component.close).not.toHaveBeenCalled();
|
||||
expect(selectItem).toBeUndefined();
|
||||
expect(spectator.component.listKeyManager.onKeydown).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should not call close, listKeyManager.onKeyDown and not emit selectItem if opened is false', () => {
|
||||
spyOn(spectator.component, 'close');
|
||||
spyOn(spectator.component.listKeyManager, 'onKeydown');
|
||||
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test' });
|
||||
|
||||
let selectItem;
|
||||
spectator.output('selectItem').subscribe((item) => (selectItem = item));
|
||||
|
||||
spectator.component.opend = false;
|
||||
const event = new KeyboardEvent('keyup', { key: 'ArrowDown' });
|
||||
spectator.component.handleKeyboardEvent(event);
|
||||
|
||||
expect(spectator.component.close).not.toHaveBeenCalled();
|
||||
expect(selectItem).toBeUndefined();
|
||||
expect(spectator.component.listKeyManager.onKeydown).not.toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('open', () => {
|
||||
it('should set opened to true', () => {
|
||||
spectator.component.open();
|
||||
expect(spectator.component.opend).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('should set opened to false and call listKeyManager.setActiveItem(undefined)', () => {
|
||||
spyOn(spectator.component.listKeyManager, 'setActiveItem');
|
||||
spectator.component.close();
|
||||
expect(spectator.component.opend).toBe(false);
|
||||
expect(spectator.component.listKeyManager.setActiveItem).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerItemOnClick()', () => {
|
||||
it('should call registerOnClick for each item', () => {
|
||||
const items = spectator.component.items;
|
||||
items.forEach((item) => spyOn(item, 'registerOnClick'));
|
||||
spectator.component.registerItemOnClick();
|
||||
items.forEach((item) => expect(item.registerOnClick).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
});
|
||||
88
apps/ui/autocomplete/src/lib/autocomplete.component.ts
Normal file
88
apps/ui/autocomplete/src/lib/autocomplete.component.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
ContentChildren,
|
||||
AfterContentInit,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ChangeDetectorRef,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { UiAutocompleteItemComponent } from './autocomplete-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-autocomplete',
|
||||
templateUrl: 'autocomplete.component.html',
|
||||
styleUrls: ['autocomplete.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs: 'uiAutocomplete',
|
||||
})
|
||||
export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
|
||||
@ContentChildren(UiAutocompleteItemComponent)
|
||||
items: QueryList<UiAutocompleteItemComponent>;
|
||||
|
||||
@Output()
|
||||
selectItem = new EventEmitter<any>();
|
||||
|
||||
get activeItem() {
|
||||
return this.listKeyManager?.activeItem;
|
||||
}
|
||||
|
||||
opend = false;
|
||||
|
||||
listKeyManager: ActiveDescendantKeyManager<UiAutocompleteItemComponent>;
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.listKeyManager = new ActiveDescendantKeyManager(this.items);
|
||||
this.listKeyManager.withWrap(true);
|
||||
this.registerItemOnClick();
|
||||
this.subscriptions.add(this.items.changes.subscribe(() => this.registerItemOnClick()));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions?.unsubscribe();
|
||||
}
|
||||
|
||||
registerItemOnClick() {
|
||||
this.items.forEach((item) =>
|
||||
item.registerOnClick((aitem) => {
|
||||
this.selectItem.emit(aitem);
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
if (this.opend) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
this.selectItem.emit(this.activeItem);
|
||||
case 'Escape':
|
||||
this.close();
|
||||
break;
|
||||
default:
|
||||
this.listKeyManager.onKeydown(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.opend = true;
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.opend = false;
|
||||
this.listKeyManager.setActiveItem(undefined);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
12
apps/ui/autocomplete/src/lib/autocomplete.module.ts
Normal file
12
apps/ui/autocomplete/src/lib/autocomplete.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiAutocompleteComponent } from './autocomplete.component';
|
||||
import { UiAutocompleteItemComponent } from './autocomplete-item.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [UiAutocompleteComponent, UiAutocompleteItemComponent],
|
||||
declarations: [UiAutocompleteComponent, UiAutocompleteItemComponent],
|
||||
})
|
||||
export class UiAutocompleteModule {}
|
||||
30
apps/ui/autocomplete/src/lib/autocomplete.scss
Normal file
30
apps/ui/autocomplete/src/lib/autocomplete.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
ui-autocomplete {
|
||||
@apply block relative z-dropdown;
|
||||
}
|
||||
|
||||
ui-autocomplete .ui-autocomplete-output-wrapper {
|
||||
@apply flex flex-col shadow-input absolute left-0 right-0;
|
||||
}
|
||||
|
||||
ui-searchbox ui-autocomplete {
|
||||
@apply shadow-none;
|
||||
|
||||
.ui-autocomplete-output-wrapper {
|
||||
@apply rounded-b-autocomplete;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-autocomplete-item {
|
||||
@apply border-none outline-none bg-transparent text-base font-sans px-4 py-5 text-left bg-white;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
@apply bg-customer;
|
||||
}
|
||||
}
|
||||
|
||||
ui-searchbox .ui-autocomplete-item {
|
||||
&:last-child {
|
||||
@apply rounded-b-autocomplete;
|
||||
}
|
||||
}
|
||||
5
apps/ui/autocomplete/src/lib/index.ts
Normal file
5
apps/ui/autocomplete/src/lib/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// start:ng42.barrel
|
||||
export * from './autocomplete-item.component';
|
||||
export * from './autocomplete.component';
|
||||
export * from './autocomplete.module';
|
||||
// end:ng42.barrel
|
||||
5
apps/ui/autocomplete/src/public-api.ts
Normal file
5
apps/ui/autocomplete/src/public-api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
* Public API Surface of autocomplete
|
||||
*/
|
||||
|
||||
export * from './lib';
|
||||
24
apps/ui/autocomplete/src/test.ts
Normal file
24
apps/ui/autocomplete/src/test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone';
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(
|
||||
path: string,
|
||||
deep?: boolean,
|
||||
filter?: RegExp
|
||||
): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
25
apps/ui/autocomplete/tsconfig.lib.json
Normal file
25
apps/ui/autocomplete/tsconfig.lib.json
Normal file
@@ -0,0 +1,25 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../out-tsc/lib",
|
||||
"target": "es2015",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": [],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2018"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"skipTemplateCodegen": true,
|
||||
"strictMetadataEmit": true,
|
||||
"enableResourceInlining": true
|
||||
},
|
||||
"exclude": [
|
||||
"src/test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
10
apps/ui/autocomplete/tsconfig.lib.prod.json
Normal file
10
apps/ui/autocomplete/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableIvy": false
|
||||
}
|
||||
}
|
||||
17
apps/ui/autocomplete/tsconfig.spec.json
Normal file
17
apps/ui/autocomplete/tsconfig.spec.json
Normal file
@@ -0,0 +1,17 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
17
apps/ui/autocomplete/tslint.json
Normal file
17
apps/ui/autocomplete/tslint.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../../tslint.json",
|
||||
"rules": {
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"ui",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"ui",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ export * from './selected-filter-options/components';
|
||||
export * from './selected-filter-options/pipe';
|
||||
export * from './mapping.service';
|
||||
export * from './utils';
|
||||
export * from './next';
|
||||
// end:ng42.barrel
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<div>
|
||||
<div class="inputs">
|
||||
<button
|
||||
class="ui-input"
|
||||
type="button"
|
||||
*ngFor="let input of uiInputGroup?.input"
|
||||
[class.active]="activeInput === input"
|
||||
[class.has-options]="input?.hasSelectedOptions() || input?.hasUnselectedOptions()"
|
||||
(click)="setActiveInput(input)"
|
||||
>
|
||||
<div class="grow">
|
||||
{{ input?.label }}
|
||||
</div>
|
||||
<ui-icon icon="arrow_head" size="1rem"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ui-filter-input-options
|
||||
[class.remove-rounded-top-left]="isFirstInputSelected"
|
||||
*ngIf="activeInput?.options"
|
||||
class="options"
|
||||
[inputOptions]="activeInput?.options"
|
||||
></ui-filter-input-options>
|
||||
@@ -0,0 +1,48 @@
|
||||
:host {
|
||||
@apply grid grid-flow-col gap-2;
|
||||
grid-template-columns: 240px 1fr;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply transition transform text-cool-grey;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
@apply grid grid-flow-row gap-2;
|
||||
|
||||
.ui-input {
|
||||
@apply flex flex-row items-center border-none outline-none p-4 font-bold text-base bg-white text-left rounded-card transition transform;
|
||||
}
|
||||
|
||||
.has-options {
|
||||
@apply text-white bg-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-input.active {
|
||||
@apply text-black bg-white pr-6 -mr-2 rounded-r-none;
|
||||
|
||||
ui-icon {
|
||||
@apply rotate-180 text-cool-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.space-right {
|
||||
@apply mr-4;
|
||||
}
|
||||
|
||||
.grow {
|
||||
@apply flex-grow;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-filter-filter-group-filter .input-options-wrapper {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
::ng-deep ui-filter-filter-group-filter .remove-rounded-top-left .input-options-wrapper {
|
||||
@apply rounded-tl-none;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiFilterInputOptionsModule } from '../../shared/filter-input-options';
|
||||
import { UiFilterFilterGroupFilterComponent } from './filter-filter-group-filter.component';
|
||||
|
||||
describe('UiFilterFilterGroupFilterComponent', () => {
|
||||
let spectator: Spectator<UiFilterFilterGroupFilterComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: UiFilterFilterGroupFilterComponent,
|
||||
imports: [UiFilterInputOptionsModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { IUiInputGroup, UiInput, UiInputGroup } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-filter-filter-group-filter',
|
||||
templateUrl: 'filter-filter-group-filter.component.html',
|
||||
styleUrls: ['filter-filter-group-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterFilterGroupFilterComponent {
|
||||
private _inputGroup: UiInputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IUiInputGroup) {
|
||||
if (value instanceof UiInputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = UiInputGroup.create(value);
|
||||
}
|
||||
}
|
||||
|
||||
get uiInputGroup() {
|
||||
return this._inputGroup;
|
||||
}
|
||||
|
||||
private _activeInput: UiInput;
|
||||
|
||||
get activeInput() {
|
||||
return this.uiInputGroup?.input?.find((f) => f?.key === this._activeInput?.key) || this.uiInputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
get isFirstInputSelected() {
|
||||
return this.activeInput === this.uiInputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
setActiveInput(input: UiInput) {
|
||||
this._activeInput = input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterFilterGroupFilterComponent } from './filter-filter-group-filter.component';
|
||||
import { UiFilterInputOptionsModule } from '../../shared/filter-input-options';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule, UiFilterInputOptionsModule],
|
||||
exports: [UiFilterFilterGroupFilterComponent],
|
||||
declarations: [UiFilterFilterGroupFilterComponent],
|
||||
})
|
||||
export class UiFilterFilterGroupFilterModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-filter-group-filter.component';
|
||||
export * from './filter-filter-group-filter.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1 @@
|
||||
<ui-filter-input-chip *ngFor="let input of uiInputGroup?.input" [input]="input"></ui-filter-input-chip>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-flow-col items-center justify-center gap-4;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiFilterInputChipModule } from '../../shared/filter-input-chip';
|
||||
import { UiInputGroup } from '../../tree';
|
||||
import { UiFilterFilterGroupMainComponent } from './filter-filter-group-main.component';
|
||||
|
||||
describe('UiFilterFilterGroupMainComponent', () => {
|
||||
let spectator: Spectator<UiFilterFilterGroupMainComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: UiFilterFilterGroupMainComponent,
|
||||
imports: [UiFilterInputChipModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('inputGroup', () => {
|
||||
it('should create an instance of UiInputGroup when value is a plain object', () => {
|
||||
spectator.setInput({ inputGroup: {} });
|
||||
expect(spectator.component.uiInputGroup instanceof UiInputGroup).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the UiInputGroup if it is already an instance of UiInputGroup', () => {
|
||||
const instance = UiInputGroup.create({});
|
||||
spectator.setInput({ inputGroup: instance });
|
||||
expect(spectator.component.uiInputGroup).toBe(instance);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui-filter-input-chip Element', () => {
|
||||
it('should render an Element for each uiInputGroup.input', () => {
|
||||
const instance = UiInputGroup.create({
|
||||
input: [
|
||||
{ label: 'Label 1', type: 0 },
|
||||
{ label: 'label 2', type: 0 },
|
||||
{ label: 'label 3', type: 0 },
|
||||
],
|
||||
});
|
||||
spectator.setInput({ inputGroup: instance });
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
expect(spectator.queryAll('ui-filter-input-chip').length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { IUiInputGroup, UiInputGroup } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-filter-filter-group-main',
|
||||
templateUrl: 'filter-filter-group-main.component.html',
|
||||
styleUrls: ['filter-filter-group-main.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterFilterGroupMainComponent {
|
||||
private _inputGroup: UiInputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IUiInputGroup) {
|
||||
if (value instanceof UiInputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = UiInputGroup.create(value);
|
||||
}
|
||||
}
|
||||
|
||||
get uiInputGroup() {
|
||||
return this._inputGroup;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterFilterGroupMainComponent } from './filter-filter-group-main.component';
|
||||
import { UiFilterInputChipModule } from '../../shared/filter-input-chip';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiFilterInputChipModule],
|
||||
exports: [UiFilterFilterGroupMainComponent],
|
||||
declarations: [UiFilterFilterGroupMainComponent],
|
||||
})
|
||||
export class UiFilterFilterGroupMainModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-filter-group-main.component';
|
||||
export * from './filter-filter-group-main.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,29 @@
|
||||
<ui-searchbox
|
||||
[placeholder]="uiInput?.placeholder"
|
||||
[query]="uiInput?.value"
|
||||
(queryChange)="uiInput?.setValue($event)"
|
||||
(complete)="complete.next($event)"
|
||||
(search)="search.emit($event)"
|
||||
[loading]="loading"
|
||||
[hint]="hint"
|
||||
[scanProvider]="scanProvider"
|
||||
>
|
||||
<ui-autocomplete *ngIf="autocompleteProvider">
|
||||
<button *ngFor="let item of autocompleteResults$ | async" [uiAutocompleteItem]="item.query">
|
||||
{{ item.display }}
|
||||
</button>
|
||||
</ui-autocomplete>
|
||||
</ui-searchbox>
|
||||
<ng-container *ngIf="showDescription && uiInput?.description">
|
||||
<button
|
||||
[uiTooltip]="tooltipContent"
|
||||
[uiTooltipPosition]="{ offsetX: 20, offsetY: -20, originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' }"
|
||||
class="info-tooltip-button"
|
||||
type="button"
|
||||
>
|
||||
i
|
||||
</button>
|
||||
<ng-template #tooltipContent>
|
||||
{{ uiInput.description }}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.info-tooltip-button {
|
||||
@apply border-font-customer bg-white rounded-md text-base font-bold absolute;
|
||||
border-style: outset;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
top: 14px;
|
||||
right: -45px;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiAutocompleteModule } from '../../../autocomplete';
|
||||
import { UiSearchboxModule } from '../../../searchbox';
|
||||
import { IUiInput, IUiInputGroup, UiInput, UiInputGroup, UiInputType } from '../../tree';
|
||||
|
||||
import { isObservable, NEVER, of } from 'rxjs';
|
||||
import { UiFilterInputGroupMainComponent } from './filter-input-group-main.component';
|
||||
|
||||
describe('UiFilterInputGroupMainComponent', () => {
|
||||
const inputGroupData: IUiInputGroup = {
|
||||
input: [{ key: 'qs', placeholder: 'ISBN/EAN, Titel, Kundenname', type: 1, constraint: '^.{2,100}$' }],
|
||||
};
|
||||
|
||||
let spectator: Spectator<UiFilterInputGroupMainComponent>;
|
||||
|
||||
const createSpectator = createComponentFactory({
|
||||
component: UiFilterInputGroupMainComponent,
|
||||
imports: [UiSearchboxModule, UiAutocompleteModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createSpectator();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('uiInput', () => {
|
||||
it('should return undefined when inputGroup is not set', () => {
|
||||
expect(spectator.component.uiInput).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an instance of UiInput', () => {
|
||||
spectator.setInput({
|
||||
inputGroup: inputGroupData,
|
||||
});
|
||||
expect(spectator.component.uiInput instanceof UiInput).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the same instance of the input', () => {
|
||||
const instance = UiInputGroup.create(inputGroupData);
|
||||
spectator.setInput({
|
||||
inputGroup: instance,
|
||||
});
|
||||
|
||||
expect(spectator.component.uiInput).toBe(instance.input[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit()', () => {
|
||||
it('should set the autocompleteProvider with the property for to be main', () => {
|
||||
const provider = { for: 'main', complete: () => NEVER };
|
||||
spectator.component['autocompleteProviders'] = [provider, { for: 'not main', complete: () => NEVER }];
|
||||
spectator.component.ngOnInit();
|
||||
expect(spectator.component.autocompleteProvider).toBe(provider);
|
||||
});
|
||||
|
||||
it('should set the scanProvider with the property for to be main', () => {
|
||||
const provider = { for: 'main', scan: () => NEVER };
|
||||
spectator.component['scanProviders'] = [provider, { for: 'not main', scan: () => NEVER }];
|
||||
spectator.component.ngOnInit();
|
||||
expect(spectator.component.scanProvider).toBe(provider);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initAutocomplete()', () => {
|
||||
it('should create an Observable for autocompleteResult$', () => {
|
||||
spectator.setInput({
|
||||
inputGroup: inputGroupData,
|
||||
});
|
||||
spectator.component.setAutocompleteProvider({ for: 'main', complete: () => NEVER });
|
||||
spectator.component.initAutocomplete();
|
||||
expect(isObservable(spectator.component.autocompleteResults$)).toBe(true);
|
||||
});
|
||||
|
||||
it('should call autocompleteComponent.open() if the prvider returns at least one result', () => {
|
||||
spectator.setInput({
|
||||
inputGroup: inputGroupData,
|
||||
});
|
||||
spectator.component.setAutocompleteProvider({
|
||||
for: 'main',
|
||||
complete: (query) => of([{ display: `${query} first` }, { display: `${query} second` }]),
|
||||
});
|
||||
spectator.detectComponentChanges();
|
||||
spyOn(spectator.component.autocompleteComponent, 'open');
|
||||
spectator.component.complete.next('test');
|
||||
expect(spectator.component.autocompleteComponent.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call autocompleteComponent.close() if the prvider returns at no results', () => {
|
||||
spectator.setInput({
|
||||
inputGroup: inputGroupData,
|
||||
});
|
||||
spectator.component.setAutocompleteProvider({
|
||||
for: 'main',
|
||||
complete: (query) => of([]),
|
||||
});
|
||||
spectator.detectComponentChanges();
|
||||
spyOn(spectator.component.autocompleteComponent, 'close');
|
||||
spectator.component.complete.next('test');
|
||||
expect(spectator.component.autocompleteComponent.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui-searchbox', () => {
|
||||
describe('input.ui-searchbox-input', () => {
|
||||
it('should have a placeholder', () => {
|
||||
spectator.setInput({
|
||||
inputGroup: { input: [{ type: UiInputType.Text, placeholder: 'Unit Test Placeholder' }] },
|
||||
});
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
expect(spectator.query('input.ui-searchbox-input')).toContainProperty('placeholder', 'Unit Test Placeholder');
|
||||
});
|
||||
|
||||
it('should have a value', async () => {
|
||||
spectator.setInput({
|
||||
inputGroup: { input: [{ type: UiInputType.Text, value: 'Unit Test Value' }] },
|
||||
});
|
||||
|
||||
spectator.detectComponentChanges();
|
||||
await spectator.fixture.whenStable();
|
||||
expect(spectator.query('input.ui-searchbox-input')).toHaveValue('Unit Test Value');
|
||||
});
|
||||
|
||||
it('should call input.setValue when value changes', async () => {
|
||||
spectator.setInput({
|
||||
inputGroup: { input: [{ type: UiInputType.Text, value: 'Unit Test Value' }] },
|
||||
});
|
||||
spyOn(spectator.component.uiInput, 'setValue');
|
||||
spectator.detectComponentChanges();
|
||||
await spectator.fixture.whenStable();
|
||||
spectator.typeInElement('Unit Test', spectator.query('input.ui-searchbox-input'));
|
||||
expect(spectator.component.uiInput.setValue).toHaveBeenCalledWith('Unit Test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui-autocomplete', () => {
|
||||
it('should not render the element if no autocompleteProvider is set', () => {
|
||||
spyOnProperty(spectator.component, 'autocompleteProvider', 'get').and.returnValue(undefined);
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('ui-autocomplete')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render the element if autocompleteProvider is set', () => {
|
||||
spectator.setInput({ inputGroup: inputGroupData });
|
||||
spyOnProperty(spectator.component, 'autocompleteProvider', 'get').and.returnValue({ for: 'main', complete: () => NEVER });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('ui-autocomplete')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Optional,
|
||||
ViewChild,
|
||||
AfterViewInit,
|
||||
OnInit,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { Observable, Subject, Subscription } from 'rxjs';
|
||||
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiFilterScanProvider } from '../../providers';
|
||||
import { IUiInputGroup, UiInput, UiInputGroup } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-filter-input-group-main',
|
||||
templateUrl: 'filter-input-group-main.component.html',
|
||||
styleUrls: ['filter-input-group-main.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterInputGroupMainComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChild(UiAutocompleteComponent, { read: UiAutocompleteComponent, static: false })
|
||||
autocompleteComponent: UiAutocompleteComponent;
|
||||
|
||||
@Output()
|
||||
search = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
loading: boolean;
|
||||
|
||||
@Input()
|
||||
hint: string;
|
||||
|
||||
@Input()
|
||||
showDescription: boolean = true;
|
||||
|
||||
private _inputGroup: UiInputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IUiInputGroup) {
|
||||
if (value instanceof UiInputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = UiInputGroup.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
this.initAutocomplete();
|
||||
}
|
||||
|
||||
get uiInput(): UiInput {
|
||||
return this._inputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
private _scanProvider: UiFilterScanProvider;
|
||||
get scanProvider() {
|
||||
return this._scanProvider;
|
||||
}
|
||||
|
||||
private _autocompleteProvider: UiFilterAutocompleteProvider;
|
||||
get autocompleteProvider() {
|
||||
return this._autocompleteProvider;
|
||||
}
|
||||
|
||||
autocompleteResults$: Observable<UiFilterAutocomplete[]>;
|
||||
|
||||
complete = new Subject<string>();
|
||||
|
||||
private changeSubscriptions: Subscription;
|
||||
|
||||
constructor(
|
||||
@Inject(UiFilterAutocompleteProvider) @Optional() private autocompleteProviders: UiFilterAutocompleteProvider[],
|
||||
@Inject(UiFilterScanProvider) @Optional() private scanProviders: UiFilterScanProvider[],
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this._autocompleteProvider = this.autocompleteProviders?.find((provider) => provider.for === 'main');
|
||||
this._scanProvider = this.scanProviders?.find((provider) => provider.for === 'main');
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.initAutocomplete();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
const sub = this.uiInput?.changes?.pipe(filter((changes) => changes?.keys.includes('value'))).subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
if (sub) {
|
||||
this.changeSubscriptions.add(sub);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.changeSubscriptions?.unsubscribe();
|
||||
this.changeSubscriptions = new Subscription();
|
||||
}
|
||||
|
||||
initAutocomplete() {
|
||||
this.autocompleteResults$ = this.complete.asObservable().pipe(
|
||||
switchMap(() => this.autocompleteProvider.complete(this.uiInput)),
|
||||
tap((complete) => {
|
||||
if (complete.length > 0) {
|
||||
this.autocompleteComponent.open();
|
||||
} else {
|
||||
this.autocompleteComponent.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setAutocompleteProvider(provider: UiFilterAutocompleteProvider) {
|
||||
this._autocompleteProvider = provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterInputGroupMainComponent } from './filter-input-group-main.component';
|
||||
import { UiAutocompleteModule } from '@ui/autocomplete';
|
||||
import { UiSearchboxNextModule } from '@ui/searchbox';
|
||||
import { UiTooltipModule } from 'apps/ui/tooltip/src/public-api';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiSearchboxNextModule, UiAutocompleteModule, UiTooltipModule],
|
||||
exports: [UiFilterInputGroupMainComponent],
|
||||
declarations: [UiFilterInputGroupMainComponent],
|
||||
})
|
||||
export class UiFilterInputGroupMainModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-group-main.component';
|
||||
export * from './filter-input-group-main.module';
|
||||
// end:ng42.barrel
|
||||
16
apps/ui/filter/src/lib/next/index.ts
Normal file
16
apps/ui/filter/src/lib/next/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// start:ng42.barrel
|
||||
export * from './ui-filter.component';
|
||||
export * from './ui-filter.module';
|
||||
export * from './filter-group/filter-filter-group-filter';
|
||||
export * from './filter-group/filter-filter-group-main';
|
||||
export * from './filter-group/filter-input-group-main';
|
||||
export * from './pipe';
|
||||
export * from './providers';
|
||||
export * from './shared/filter-input-chip';
|
||||
export * from './shared/filter-input-options';
|
||||
export * from './shared/filter-input-options/filter-input-option-bool';
|
||||
export * from './shared/filter-input-options/filter-input-option-date-range';
|
||||
export * from './shared/filter-input-options/filter-input-option-tri-state';
|
||||
export * from './testing';
|
||||
export * from './tree';
|
||||
// end:ng42.barrel
|
||||
3
apps/ui/filter/src/lib/next/pipe/index.ts
Normal file
3
apps/ui/filter/src/lib/next/pipe/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './ui-input-group-selector.pipe';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator';
|
||||
import { IUiInputGroup } from '../tree';
|
||||
import { UiInputGroupSelectorPipe } from './ui-input-group-selector.pipe';
|
||||
|
||||
describe('UiInputGroupSelectorPipe', () => {
|
||||
let spectator: SpectatorPipe<UiInputGroupSelectorPipe>;
|
||||
const createPipe = createPipeFactory(UiInputGroupSelectorPipe);
|
||||
const inputGroups: IUiInputGroup[] = [{ group: 'group1' }, { group: 'group2' }, { group: 'group3' }];
|
||||
|
||||
it('should return the IUiInputGroup with group to be unittest', () => {
|
||||
spectator = createPipe(`{{ (value | group:group)?.group }}`, {
|
||||
hostProps: { value: inputGroups, group: 'group2' },
|
||||
});
|
||||
|
||||
expect(spectator.element).toHaveText('group2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { IUiInputGroup } from '../tree';
|
||||
|
||||
@Pipe({
|
||||
name: 'group',
|
||||
})
|
||||
export class UiInputGroupSelectorPipe implements PipeTransform {
|
||||
transform(value: IUiInputGroup[], group: string): any {
|
||||
return value?.find((f) => f?.group === group);
|
||||
}
|
||||
}
|
||||
4
apps/ui/filter/src/lib/next/providers/index.ts
Normal file
4
apps/ui/filter/src/lib/next/providers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './ui-filter-autocomplete.provider';
|
||||
export * from './ui-filter-scan.provider';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UiInput } from '../tree';
|
||||
|
||||
export interface UiFilterAutocomplete {
|
||||
/**
|
||||
* Anzeige / Bezeichner
|
||||
*/
|
||||
display?: string;
|
||||
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Abfragewert
|
||||
*/
|
||||
query?: string;
|
||||
|
||||
/**
|
||||
* Art (z.B. Titel, Autor, Verlag, ...)
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export abstract class UiFilterAutocompleteProvider {
|
||||
abstract readonly for: string;
|
||||
|
||||
abstract complete(input: UiInput): Observable<UiFilterAutocomplete[]>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UiSearchboxScanProvider } from '@ui/searchbox';
|
||||
|
||||
export abstract class UiFilterScanProvider extends UiSearchboxScanProvider {
|
||||
abstract readonly for: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<button
|
||||
*ngIf="!uiInput?.hasOptions()"
|
||||
type="button"
|
||||
class="ui-filter-chip"
|
||||
[class.selected]="uiInput?.selected"
|
||||
(click)="uiInput?.setSelected(!uiInput.selected)"
|
||||
>
|
||||
{{ uiInput?.label }}
|
||||
</button>
|
||||
<ng-container *ngIf="uiInput?.hasOptions()">
|
||||
<button
|
||||
*ngFor="let option of uiInput?.options?.values"
|
||||
type="button"
|
||||
class="ui-filter-chip"
|
||||
[class.selected]="option?.selected"
|
||||
(click)="option?.setSelected(!option.selected)"
|
||||
>
|
||||
{{ option?.label }}
|
||||
</button>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,21 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
button.ui-filter-chip {
|
||||
@apply grid grid-flow-col gap-2 items-center rounded-full text-base px-4 py-3 bg-white text-dark-cerulean border-none font-bold;
|
||||
}
|
||||
|
||||
/** styling branch bereich **/
|
||||
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
|
||||
button.ui-filter-chip.selected {
|
||||
@apply bg-dark-cerulean text-white;
|
||||
}
|
||||
|
||||
/** styling branch bereich **/
|
||||
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip.selected {
|
||||
@apply bg-cool-grey;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiInput, UiInputGroup, UiInputType } from '../../tree';
|
||||
import { UiFilterInputChipComponent } from './filter-input-chip.component';
|
||||
|
||||
describe('UiFilterInputChipComponent', () => {
|
||||
let spectator: Spectator<UiFilterInputChipComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: UiFilterInputChipComponent,
|
||||
imports: [UiIconModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('input', () => {
|
||||
it('should create an instance of UiInput if value is a plain object', () => {
|
||||
spectator.setInput({
|
||||
input: { type: UiInputType.NotSet },
|
||||
});
|
||||
|
||||
expect(spectator.component.uiInput instanceof UiInput).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('button.ui-filter-chip Element', () => {
|
||||
it('should have the text content of uiInput.label', () => {
|
||||
spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ label: 'My Label', type: UiInputType.NotSet }));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('button.ui-filter-chip')).toContainText('My Label');
|
||||
});
|
||||
|
||||
it('should have the class selected if uiInput.selected is true', () => {
|
||||
spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ selected: true, type: UiInputType.NotSet }));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('button.ui-filter-chip')).toHaveClass('selected');
|
||||
});
|
||||
|
||||
it('should not have the class selected if uiInput.selected is true', () => {
|
||||
spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ selected: false, type: UiInputType.NotSet }));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('button.ui-filter-chip')).not.toHaveClass('selected');
|
||||
});
|
||||
|
||||
it('should call uiInput.setSelected(!uiInput.selected) on click', () => {
|
||||
spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ type: UiInputType.NotSet }));
|
||||
spyOn(spectator.component.uiInput, 'setSelected');
|
||||
spectator.detectComponentChanges();
|
||||
spectator.click('button.ui-filter-chip');
|
||||
expect(spectator.component.uiInput.setSelected).toHaveBeenCalledWith(!spectator.component.uiInput.selected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui-icon Element', () => {
|
||||
it('should not be visible if selected is false', () => {
|
||||
spectator.setInput({ input: { type: 0, selected: false } });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('ui-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should be visible if selected is true', () => {
|
||||
spectator.setInput({ input: { type: 0, selected: true } });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('ui-icon')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import { IUiInput, UiInput } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-filter-input-chip',
|
||||
templateUrl: 'filter-input-chip.component.html',
|
||||
styleUrls: ['filter-input-chip.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterInputChipComponent implements OnDestroy {
|
||||
private _input: UiInput;
|
||||
|
||||
@Input()
|
||||
set input(value: IUiInput) {
|
||||
if (value instanceof UiInput) {
|
||||
this._input = value;
|
||||
} else {
|
||||
this._input = UiInput.create(value);
|
||||
}
|
||||
this.registerChanges();
|
||||
}
|
||||
|
||||
get uiInput() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private changeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unregisterChanges();
|
||||
}
|
||||
|
||||
registerChanges() {
|
||||
this.unregisterChanges();
|
||||
|
||||
if (this.uiInput) {
|
||||
merge(this.uiInput.changes, ...(this.uiInput?.options?.values?.map((o) => o.changes) || [])).subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unregisterChanges() {
|
||||
this.changeSubscription?.unsubscribe();
|
||||
this.changeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterInputChipComponent } from './filter-input-chip.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [UiFilterInputChipComponent],
|
||||
declarations: [UiFilterInputChipComponent],
|
||||
})
|
||||
export class UiFilterInputChipModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-chip.component';
|
||||
export * from './filter-input-chip.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="ui-option">
|
||||
<ui-checkbox [ngModel]="uiOption?.selected" (ngModelChange)="uiOption?.setSelected($event)">
|
||||
{{ uiOption?.label }}
|
||||
</ui-checkbox>
|
||||
<button
|
||||
class="btn-expand"
|
||||
(click)="uiOption.setExpanded(!uiOption?.expanded)"
|
||||
[class.expanded]="uiOption?.expanded"
|
||||
type="button"
|
||||
*ngIf="uiOption?.values?.length"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="uiOption?.expanded">
|
||||
<ui-input-option-bool *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-bool>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row;
|
||||
}
|
||||
|
||||
.ui-option {
|
||||
@apply px-4 py-2 flex flex-row justify-between items-center;
|
||||
|
||||
.btn-expand {
|
||||
@apply border-none outline-none bg-transparent text-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-all transform rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-expand.expanded ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-bool ui-checkbox ui-icon {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiFilterInputOptionBoolComponent } from './filter-input-option-bool.component';
|
||||
|
||||
describe('UiFilterInputOptionBoolComponent', () => {
|
||||
let spectator: Spectator<UiFilterInputOptionBoolComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: UiFilterInputOptionBoolComponent,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IUiOption, UiOption } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-input-option-bool',
|
||||
templateUrl: 'filter-input-option-bool.component.html',
|
||||
styleUrls: ['filter-input-option-bool.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterInputOptionBoolComponent implements OnDestroy {
|
||||
private _option: UiOption;
|
||||
|
||||
@Input()
|
||||
set option(value: IUiOption) {
|
||||
if (value instanceof UiOption) {
|
||||
this._option = value;
|
||||
} else {
|
||||
this._option = UiOption.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOption() {
|
||||
return this._option;
|
||||
}
|
||||
|
||||
optionChangeSubscription = new Subscription();
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterInputOptionBoolComponent } from './filter-input-option-bool.component';
|
||||
import { UiCheckboxModule } from '@ui/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, UiCheckboxModule, UiIconModule],
|
||||
exports: [UiFilterInputOptionBoolComponent],
|
||||
declarations: [UiFilterInputOptionBoolComponent],
|
||||
})
|
||||
export class UiFilterInputOptionBoolModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-bool.component';
|
||||
export * from './filter-input-option-bool.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="options-wrapper">
|
||||
<div *ngIf="uiStartOption" class="option">
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStartOption?.label }}: </span>
|
||||
<button class="cta-picker" [class.open]="dpStart?.opened" type="button" (click)="dpStart?.toggle()">
|
||||
<span>
|
||||
{{ uiStartOption?.value | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker class="dp-left" #dpStart [ngModel]="uiStartOption?.value" (save)="uiStartOption?.setValue($event)"> </ui-datepicker>
|
||||
</div>
|
||||
<div *ngIf="uiStopOption" class="option">
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStopOption?.label }}: </span>
|
||||
<button class="cta-picker" [class.open]="dpStop?.opened" type="button" (click)="dpStop?.toggle()">
|
||||
<span>
|
||||
{{ uiStopOption?.value | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker class="dp-right" right="true" #dpStop [ngModel]="uiStopOption?.value" (save)="uiStopOption?.setValue($event)">
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
:host {
|
||||
@apply block p-4;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
@apply grid grid-flow-col justify-start gap-4;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply font-bold;
|
||||
|
||||
button.cta-picker {
|
||||
@apply bg-transparent text-base outline-none border-none font-bold inline-flex flex-row items-center justify-between;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply ml-2 transition transform rotate-90 text-cool-grey;
|
||||
}
|
||||
|
||||
button.cta-picker.open ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
@apply grid grid-flow-col gap-2 items-center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-date-range ui-datepicker.dp-left {
|
||||
.dp {
|
||||
left: 102px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-date-range ui-datepicker.dp-right {
|
||||
.dp {
|
||||
right: -6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { UiInput, UiInputType, UiOption } from '../../../tree';
|
||||
import { UiFilterInputOptionDateRangeComponent } from './filter-input-option-date-range.component';
|
||||
|
||||
describe('UiFilterInputOptionDateRangeComponent', () => {
|
||||
let spectator: Spectator<UiFilterInputOptionDateRangeComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: UiFilterInputOptionDateRangeComponent,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('set options(value: IUiOption[])', () => {
|
||||
it('should create an instance of UiOption when values are not an instance of UiOption', () => {
|
||||
spectator.setInput({
|
||||
options: [{ key: 'start' }, { key: 'stop' }],
|
||||
});
|
||||
|
||||
spectator.component['_options'].forEach((uiOption) => {
|
||||
expect(uiOption instanceof UiOption).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the values when vales are an isntance of UiOption', () => {
|
||||
const option1 = UiOption.create({ key: 'start' });
|
||||
const option2 = UiOption.create({ key: 'stop' });
|
||||
spectator.setInput({
|
||||
options: [option1, option2],
|
||||
});
|
||||
|
||||
expect(spectator.component['_options'][0]).toBe(option1);
|
||||
expect(spectator.component['_options'][1]).toBe(option2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get uiOptions()', () => {
|
||||
it('should return the value of _options', () => {
|
||||
const option1 = UiOption.create({ key: 'start' });
|
||||
const option2 = UiOption.create({ key: 'stop' });
|
||||
const options = [option1, option2];
|
||||
spectator.component['_options'] = options;
|
||||
|
||||
expect(spectator.component.uiOptions).toBe(options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get uiStartOption()', () => {
|
||||
it('should retun the option with the key start', () => {
|
||||
const option1 = UiOption.create({ key: 'start' });
|
||||
const option2 = UiOption.create({ key: 'stop' });
|
||||
const options = [option1, option2];
|
||||
spectator.component['_options'] = options;
|
||||
|
||||
expect(spectator.component.uiStartOption).toBe(option1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get uiStopOption()', () => {
|
||||
it('should return the option with the key stop', () => {
|
||||
const option1 = UiOption.create({ key: 'start' });
|
||||
const option2 = UiOption.create({ key: 'stop' });
|
||||
const options = [option1, option2];
|
||||
spectator.component['_options'] = options;
|
||||
|
||||
expect(spectator.component.uiStopOption).toBe(option2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IUiOption, UiOption } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-input-option-date-range',
|
||||
templateUrl: 'filter-input-option-date-range.component.html',
|
||||
styleUrls: ['filter-input-option-date-range.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiFilterInputOptionDateRangeComponent {
|
||||
private _options: UiOption[];
|
||||
|
||||
@Input()
|
||||
set options(value: IUiOption[]) {
|
||||
this._options = value?.map((option) => (option instanceof UiOption ? option : UiOption.create(option)));
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOptions() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get uiStartOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'start');
|
||||
}
|
||||
|
||||
get uiStopOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'stop');
|
||||
}
|
||||
|
||||
optionChangeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiStartOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStartOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.uiStopOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStopOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription?.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiFilterInputOptionDateRangeComponent } from './filter-input-option-date-range.component';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiDatepickerModule, FormsModule, UiIconModule],
|
||||
exports: [UiFilterInputOptionDateRangeComponent],
|
||||
declarations: [UiFilterInputOptionDateRangeComponent],
|
||||
})
|
||||
export class UiFilterInputOptionDateRangeModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-date-range.component';
|
||||
export * from './filter-input-option-date-range.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="options-wrapper">
|
||||
<div class="option" *ngIf="uiStartOption">
|
||||
<div class="option-wrapper">
|
||||
<ui-form-control [label]="uiStartOption?.label">
|
||||
<input type="text" [ngModel]="uiStartOption?.value" (ngModelChange)="uiStartOption?.setValue($event)" />
|
||||
</ui-form-control>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" *ngIf="uiStopOption">
|
||||
<div class="option-wrapper">
|
||||
<ui-form-control [label]="uiStopOption?.label">
|
||||
<input type="text" [ngModel]="uiStopOption?.value" (ngModelChange)="uiStopOption?.setValue($event)" />
|
||||
</ui-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="ui-filter-date-range-validation">
|
||||
{{ uiStartOption?.validate() || uiStopOption?.validate() }}
|
||||
</p>
|
||||
@@ -0,0 +1,16 @@
|
||||
:host {
|
||||
@apply block p-4;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
@apply grid grid-flow-col justify-start gap-4;
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
@apply grid grid-flow-col gap-2 items-center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.ui-filter-date-range-validation {
|
||||
@apply text-brand text-base font-bold;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IUiOption, UiOption } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-input-option-number-range',
|
||||
templateUrl: 'filter-input-option-number-range.component.html',
|
||||
styleUrls: ['filter-input-option-number-range.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiInputOptionNumberRangeComponent {
|
||||
private _options: UiOption[];
|
||||
|
||||
@Input()
|
||||
set options(value: IUiOption[]) {
|
||||
this._options = value?.map((option) => (option instanceof UiOption ? option : UiOption.create(option)));
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOptions() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get uiStartOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'start');
|
||||
}
|
||||
|
||||
get uiStopOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'stop');
|
||||
}
|
||||
|
||||
optionChangeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiStartOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStartOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.uiStopOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStopOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription?.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiInputOptionNumberRangeComponent } from './filter-input-option-number-range.component';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiFormControlModule, FormsModule],
|
||||
exports: [UiInputOptionNumberRangeComponent],
|
||||
declarations: [UiInputOptionNumberRangeComponent],
|
||||
})
|
||||
export class UiInputOptionNumberRangeModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-number-range.component';
|
||||
export * from './filter-input-option-number-range.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="ui-option">
|
||||
<div>
|
||||
<ui-switch [ngModel]="uiOption?.selected" (ngModelChange)="uiOption?.setSelected($event)" labelOn="mit" labelOff="ohne"> </ui-switch>
|
||||
{{ uiOption?.label }}
|
||||
</div>
|
||||
<button
|
||||
class="btn-expand"
|
||||
(click)="uiOption.setExpanded(!uiOption?.expanded)"
|
||||
[class.expanded]="uiOption?.expanded"
|
||||
type="button"
|
||||
*ngIf="uiOption?.values?.length"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="uiOption?.expanded">
|
||||
<ui-input-option-tri-state *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-tri-state>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,27 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row;
|
||||
}
|
||||
|
||||
ui-switch {
|
||||
@apply mr-2;
|
||||
}
|
||||
|
||||
.ui-option {
|
||||
@apply px-4 py-2 flex flex-row justify-between items-center;
|
||||
|
||||
.btn-expand {
|
||||
@apply border-none outline-none bg-transparent text-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-all transform rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-expand.expanded ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-bool ui-checkbox ui-icon {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, ChangeDetectionStrategy, OnDestroy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IUiOption, UiOption } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-input-option-tri-state',
|
||||
templateUrl: 'filter-input-option-tri-state.component.html',
|
||||
styleUrls: ['filter-input-option-tri-state.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UiInputOptionTriStateComponent implements OnDestroy {
|
||||
private _option: UiOption;
|
||||
|
||||
@Input()
|
||||
set option(value: IUiOption) {
|
||||
if (value instanceof UiOption) {
|
||||
this._option = value;
|
||||
} else {
|
||||
this._option = UiOption.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOption() {
|
||||
return this._option;
|
||||
}
|
||||
|
||||
optionChangeSubscription = new Subscription();
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UiInputOptionTriStateComponent } from './filter-input-option-tri-state.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSwitchModule } from '@ui/switch';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiSwitchModule, FormsModule, UiIconModule],
|
||||
exports: [UiInputOptionTriStateComponent],
|
||||
declarations: [UiInputOptionTriStateComponent],
|
||||
})
|
||||
export class UiInputOptionTriStateModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-tri-state.component';
|
||||
export * from './filter-input-option-tri-state.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="input-options-wrapper">
|
||||
<div class="hidden-overflow">
|
||||
<div class="input-options-header" [class.header-shadow]="scrollPersantage > 0">
|
||||
<button type="button" (click)="setSelected(undefined)">
|
||||
Alle entfernen
|
||||
</button>
|
||||
<button type="button" (click)="setSelected(true)" *ngIf="uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4">
|
||||
Alle auswählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-options-wrapper">
|
||||
<p class="input-desription">
|
||||
{{ uiInputOptions?.parent?.description }}
|
||||
</p>
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4">
|
||||
<div class="input-options" #inputOptionsConainter (scroll)="markForCheck()">
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 2">
|
||||
<ui-input-option-bool *ngFor="let option of uiInputOptions?.values" [option]="option"></ui-input-option-bool>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 4">
|
||||
<ui-input-option-tri-state *ngFor="let option of uiInputOptions?.values" [option]="option"></ui-input-option-tri-state>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button class="cta-scroll" [class.up]="scrollPersantage > 20" *ngIf="scrollable" (click)="scroll(20)">
|
||||
<ui-icon icon="arrow" size="20px"></ui-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ui-input-option-date-range *ngIf="uiInputOptions?.parent?.type === 128" [options]="uiInputOptions?.values">
|
||||
</ui-input-option-date-range>
|
||||
<ui-input-option-number-range *ngIf="uiInputOptions?.parent?.type === 4096" [options]="uiInputOptions?.values">
|
||||
</ui-input-option-number-range>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.input-desription {
|
||||
@apply my-0 px-4 font-bold text-base;
|
||||
}
|
||||
|
||||
.input-options-wrapper {
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply grid bg-white rounded-card;
|
||||
}
|
||||
|
||||
.hidden-overflow {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.input-options-header {
|
||||
@apply grid grid-flow-col justify-end items-center mb-2;
|
||||
|
||||
button {
|
||||
@apply bg-transparent p-4 text-base outline-none border-none font-semibold text-cool-grey;
|
||||
}
|
||||
|
||||
&.header-shadow {
|
||||
@apply shadow-card;
|
||||
}
|
||||
}
|
||||
|
||||
.input-options-wrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.input-options {
|
||||
@apply overflow-scroll;
|
||||
}
|
||||
|
||||
.cta-scroll {
|
||||
@apply absolute bottom-4 right-4 shadow-cta border-none outline-none bg-white w-10 h-10 rounded-full grid items-center justify-center text-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-transform transform rotate-90;
|
||||
}
|
||||
|
||||
&.up ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user