mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Catalog Filter Store Refactor and Caching Service
This commit is contained in:
40
angular.json
40
angular.json
@@ -2576,6 +2576,46 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@core/cache": {
|
||||
"projectType": "library",
|
||||
"root": "apps/core/cache",
|
||||
"sourceRoot": "apps/core/cache/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/core/cache/tsconfig.lib.json",
|
||||
"project": "apps/core/cache/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/core/cache/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "apps/core/cache/src/test.ts",
|
||||
"tsConfig": "apps/core/cache/tsconfig.spec.json",
|
||||
"karmaConfig": "apps/core/cache/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"apps/core/cache/tsconfig.lib.json",
|
||||
"apps/core/cache/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "sales"
|
||||
|
||||
25
apps/core/cache/README.md
vendored
Normal file
25
apps/core/cache/README.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Cache
|
||||
|
||||
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 cache` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project cache`.
|
||||
|
||||
> Note: Don't forget to add `--project cache` or else it will be added to the default project in your `angular.json` file.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build cache` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Publishing
|
||||
|
||||
After building your library with `ng build cache`, go to the dist folder `cd dist/cache` and run `npm publish`.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test cache` 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/core/cache/karma.conf.js
vendored
Normal file
32
apps/core/cache/karma.conf.js
vendored
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/core/cache'),
|
||||
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/core/cache/ng-package.json
vendored
Normal file
7
apps/core/cache/ng-package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../../dist/core/cache",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
11
apps/core/cache/package.json
vendored
Normal file
11
apps/core/cache/package.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@core/cache",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^10.1.2",
|
||||
"@angular/core": "^10.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
}
|
||||
4
apps/core/cache/src/lib/cache-options.ts
vendored
Normal file
4
apps/core/cache/src/lib/cache-options.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
persist?: boolean;
|
||||
}
|
||||
8
apps/core/cache/src/lib/cache.module.ts
vendored
Normal file
8
apps/core/cache/src/lib/cache.module.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
})
|
||||
export class CacheModule {}
|
||||
73
apps/core/cache/src/lib/cache.service.ts
vendored
Normal file
73
apps/core/cache/src/lib/cache.service.ts
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CacheOptions } from './cache-options';
|
||||
import { Cached } from './cached';
|
||||
import { sha1 } from 'object-hash';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CacheService {
|
||||
constructor() {}
|
||||
|
||||
set(token: Object, data: any, options?: CacheOptions) {
|
||||
const persist = options?.persist;
|
||||
const ttl = options?.ttl;
|
||||
const cached: Cached = {
|
||||
data,
|
||||
};
|
||||
|
||||
if (ttl) {
|
||||
cached.until = Date.now() + ttl;
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
localStorage.setItem(this.getKey(token), this.serialize(cached));
|
||||
} else {
|
||||
sessionStorage.setItem(this.getKey(token), this.serialize(cached));
|
||||
}
|
||||
|
||||
Object.freeze(cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
get<T = any>(token: Object, from: 'session' | 'persist' = 'session'): T {
|
||||
let cached: Cached;
|
||||
|
||||
if (from === 'session') {
|
||||
cached = this.deserialize(sessionStorage.getItem(this.getKey(token)));
|
||||
} else if (from === 'persist') {
|
||||
cached = this.deserialize(localStorage.getItem(this.getKey(token)));
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cached.until < Date.now()) {
|
||||
this.delete(token, from);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
private delete(token: Object, from: 'session' | 'persist' = 'session') {
|
||||
if (from === 'session') {
|
||||
sessionStorage.removeItem(this.getKey(token));
|
||||
} else if (from === 'persist') {
|
||||
localStorage.removeItem(this.getKey(token));
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(token: Object) {
|
||||
return sha1(token);
|
||||
}
|
||||
|
||||
private serialize(data: Cached): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
private deserialize(data: string): Cached {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
4
apps/core/cache/src/lib/cached.ts
vendored
Normal file
4
apps/core/cache/src/lib/cached.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Cached {
|
||||
until?: number;
|
||||
data?: any;
|
||||
}
|
||||
6
apps/core/cache/src/public-api.ts
vendored
Normal file
6
apps/core/cache/src/public-api.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Public API Surface of cache
|
||||
*/
|
||||
|
||||
export * from './lib/cache.service';
|
||||
export * from './lib/cache.module';
|
||||
24
apps/core/cache/src/test.ts
vendored
Normal file
24
apps/core/cache/src/test.ts
vendored
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/core/cache/tsconfig.lib.json
vendored
Normal file
25
apps/core/cache/tsconfig.lib.json
vendored
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/core/cache/tsconfig.lib.prod.json
vendored
Normal file
10
apps/core/cache/tsconfig.lib.prod.json
vendored
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/core/cache/tsconfig.spec.json
vendored
Normal file
17
apps/core/cache/tsconfig.spec.json
vendored
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/core/cache/tslint.json
vendored
Normal file
17
apps/core/cache/tslint.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../../tslint.json",
|
||||
"rules": {
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"lib",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"lib",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,17 @@ export class DomainCatalogService {
|
||||
}
|
||||
|
||||
getSearchHistory({ take }: { take: number }) {
|
||||
return this.searchService.SearchHistory(take ?? 10).pipe(map((res) => res.result));
|
||||
return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
search({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService.SearchSearch({
|
||||
queryToken,
|
||||
stockId: null,
|
||||
});
|
||||
return this.searchService
|
||||
.SearchSearch({
|
||||
queryToken,
|
||||
stockId: null,
|
||||
})
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
getDetailsById({ id }: { id: number }) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { StringDictionary } from '@cmf/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { first, map, tap, shareReplay, switchMap, throttleTime, withLatestFrom, debounceTime, take } from 'rxjs/operators';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { first, map, tap, switchMap, withLatestFrom, debounceTime, finalize } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import {
|
||||
fromInputDto,
|
||||
@@ -36,6 +36,8 @@ export interface ArticleSearchState {
|
||||
/* tslint:disable member-ordering */
|
||||
@Injectable()
|
||||
export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
readonly onSearch = new Subject<{ clear?: boolean; reload?: boolean }>();
|
||||
|
||||
private queryParamsQuerySelector = (s: ArticleSearchState) => (s.params?.query?.length ? decodeURI(s.params.query) : '');
|
||||
readonly queryParamsQuery$ = this.select(this.queryParamsQuerySelector);
|
||||
get query() {
|
||||
@@ -43,7 +45,10 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
}
|
||||
|
||||
private itemsSelector = (s: ArticleSearchState) => s.items ?? [];
|
||||
readonly items$ = this.select(this.itemsSelector).pipe(tap(console.log.bind(window, 'items')));
|
||||
readonly items$ = this.select(this.itemsSelector);
|
||||
get items() {
|
||||
return this.get(this.itemsSelector);
|
||||
}
|
||||
|
||||
private orderBySelector = (s: ArticleSearchState) => (s.params?.orderBy != undefined ? decodeURI(s.params.orderBy) : undefined);
|
||||
readonly queryParamsOrderBy$ = this.select(this.orderBySelector);
|
||||
@@ -61,7 +66,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
readonly searchState$ = this.select(this.searchStateSelector);
|
||||
|
||||
private queryParamsSelector = (s: ArticleSearchState) =>
|
||||
Object.keys(s.params).reduce((dic, key) => ({ ...dic, [key]: encodeURI(s.params[key]) }), {} as StringDictionary<string>);
|
||||
Object.keys(s.params).reduce((dic, key) => ({ ...dic, [key]: s.params[key] }), {} as StringDictionary<string>);
|
||||
readonly queryParams$ = this.select(this.queryParamsSelector);
|
||||
get queryParams() {
|
||||
return this.get(this.queryParamsSelector);
|
||||
@@ -84,30 +89,25 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
main: ig.find((g) => g.group === 'main')?.input.map(fromInputDto),
|
||||
inputSelector: ig.find((g) => g.group === 'input_selector')?.input.map(fromInputDto),
|
||||
};
|
||||
}),
|
||||
shareReplay()
|
||||
})
|
||||
);
|
||||
|
||||
readonly orderByOptions$ = this.catalog.getOrderBy();
|
||||
|
||||
readonly filter$ = combineLatest([this.queryParamsFilter$, this.defaultFilter$]).pipe(
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.filter)),
|
||||
shareReplay()
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.filter))
|
||||
);
|
||||
|
||||
readonly inputSelectorFilter$ = combineLatest([this.queryParamsInputSelector$, this.defaultFilter$]).pipe(
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.inputSelector)),
|
||||
shareReplay()
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.inputSelector))
|
||||
);
|
||||
|
||||
readonly mainFilter$ = combineLatest([this.queryParamsMain$, this.defaultFilter$]).pipe(
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.main)),
|
||||
shareReplay()
|
||||
map(([selectedFilters, defaultFilters]) => mapSelectedParamsToFilter(selectedFilters, defaultFilters.main))
|
||||
);
|
||||
|
||||
readonly queryTokenFilter$ = combineLatest([this.filter$, this.mainFilter$]).pipe(
|
||||
map(([filter, mainFilter]) => mapFilterArrayToStringDictionary([...filter, ...mainFilter])),
|
||||
shareReplay()
|
||||
map(([filter, mainFilter]) => mapFilterArrayToStringDictionary([...filter, ...mainFilter]))
|
||||
);
|
||||
|
||||
readonly queryTokenInput$ = combineLatest([this.queryParamsQuery$, this.inputSelectorFilter$]).pipe(
|
||||
@@ -127,19 +127,14 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
}
|
||||
|
||||
return dic;
|
||||
}),
|
||||
shareReplay()
|
||||
})
|
||||
);
|
||||
|
||||
readonly orderBy$ = combineLatest([this.queryParamsOrderBy$, this.queryParamsDesc$, this.orderByOptions$]).pipe(
|
||||
map(([orderBy, desc, orderByOptions]) => orderByOptions.find((opt) => opt.by === orderBy && !!opt.desc === Boolean(desc))),
|
||||
shareReplay()
|
||||
map(([orderBy, desc, orderByOptions]) => orderByOptions.find((opt) => opt.by === orderBy && !!opt.desc === Boolean(desc)))
|
||||
);
|
||||
|
||||
readonly queryTokenOrderBy$ = this.orderBy$.pipe(
|
||||
map((orderBy) => (orderBy ? [orderBy] : undefined)),
|
||||
shareReplay()
|
||||
);
|
||||
readonly queryTokenOrderBy$ = this.orderBy$.pipe(map((orderBy) => (orderBy ? [orderBy] : undefined)));
|
||||
|
||||
readonly queryToken$ = combineLatest([
|
||||
this.queryTokenInput$,
|
||||
@@ -156,9 +151,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
returnStockData: false,
|
||||
friendlyName,
|
||||
} as QueryTokenDTO)
|
||||
),
|
||||
shareReplay(),
|
||||
tap(console.log.bind(window, 'queryParams'))
|
||||
)
|
||||
);
|
||||
|
||||
private connectedRouteSubscription: Subscription;
|
||||
@@ -176,9 +169,13 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
}
|
||||
|
||||
search = this.effect((options$: Observable<{ clear?: boolean; reload?: boolean }>) =>
|
||||
combineLatest([options$, this.queryToken$, this.items$]).pipe(
|
||||
options$.pipe(
|
||||
debounceTime(250),
|
||||
first(),
|
||||
withLatestFrom(this.queryToken$, this.items$),
|
||||
tap(([options]) => {
|
||||
this.setSearchState({ searchState: 'fetching' });
|
||||
this.onSearch.next(options);
|
||||
}),
|
||||
switchMap(([options, queryToken, items]) =>
|
||||
this.catalog
|
||||
.search({
|
||||
@@ -191,6 +188,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
this.setSearchState({ searchState: '' });
|
||||
if (options.clear || options.reload) {
|
||||
this.patchState({ items: res.result, hits: res.hits });
|
||||
if (res.hits > 1) {
|
||||
@@ -199,6 +197,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
this.navigateToDetails(res.result[0]);
|
||||
} else {
|
||||
this.setSearchState({ searchState: 'empty' });
|
||||
this.navigateToMain();
|
||||
}
|
||||
} else {
|
||||
this.patchState({ items: [...items, ...res.result], hits: res.hits });
|
||||
@@ -216,7 +215,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
this.disconnect();
|
||||
let connected = false;
|
||||
|
||||
this.connectedRouteSubscription = route.queryParams.subscribe((params) => {
|
||||
this.connectedRouteSubscription = route.queryParams.pipe(finalize(() => options?.disconnected?.call(undefined))).subscribe((params) => {
|
||||
const current = this.get(this.queryParamsSelector);
|
||||
|
||||
if (!isEqual(current, params)) {
|
||||
@@ -225,14 +224,13 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
|
||||
if (!connected) {
|
||||
connected = true;
|
||||
options?.connected?.call(undefined, params);
|
||||
setTimeout(() => options?.connected?.call(undefined, params), 0);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disconnect: () => {
|
||||
this.disconnect();
|
||||
options?.disconnected?.call(undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -275,6 +273,10 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
this.router.navigate(['/product/details', item.id]);
|
||||
}
|
||||
|
||||
navigateToMain() {
|
||||
this.router.navigate(['/product/search'], { queryParams: this.queryParams });
|
||||
}
|
||||
|
||||
private setQueryParams({ params }: { params: StringDictionary<string> }) {
|
||||
this.patchState({ params });
|
||||
}
|
||||
@@ -293,6 +295,7 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
|
||||
setFilter({ filter }: { filter: Filter[] }) {
|
||||
const queryParams = this.get(this.queryParamsSelector);
|
||||
|
||||
this.patchState({
|
||||
params: {
|
||||
...queryParams,
|
||||
@@ -331,6 +334,10 @@ export class ArticleSearchStore extends ComponentStore<ArticleSearchState> {
|
||||
this.patchState({ searchState });
|
||||
}
|
||||
|
||||
setItems({ items }: { items: ItemDTO[] }) {
|
||||
this.patchState({ items });
|
||||
}
|
||||
|
||||
setOrderBy({ orderBy }: { orderBy: OrderByDTO }) {
|
||||
const queryParams = this.get(this.queryParamsSelector);
|
||||
this.patchState({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<button class="filter" [class.active]="anyFiltersActive$ | async" (click)="filterActive$.next(true)">
|
||||
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true)">
|
||||
<ui-icon size="20px" icon="filter_alit"></ui-icon>
|
||||
<span class="label">Filter</span>
|
||||
</button>
|
||||
|
||||
@@ -15,30 +15,12 @@ export class ArticleSearchComponent implements OnInit {
|
||||
filterActive$ = new BehaviorSubject<boolean>(false);
|
||||
showMainContent$ = this.getShowMainContent();
|
||||
lastSelectedFilterCategory: Filter;
|
||||
anyFiltersActive$: Observable<boolean>;
|
||||
|
||||
// readonly filters$ = this.articleSearchStore.selectFilter$;
|
||||
hasFilter$: Observable<boolean> = this.store.queryParamsFilter$.pipe(map((filter) => filter !== ''));
|
||||
|
||||
constructor(private articleSearchStore: ArticleSearchStore) {}
|
||||
constructor(private store: ArticleSearchStore) {}
|
||||
|
||||
ngOnInit() {
|
||||
// this.articleSearchStore.loadInitialFilters();
|
||||
// this.articleSearchStore.searchHistory();
|
||||
// this.anyFiltersActive$ = this.filters$.pipe(map((filters) => this.anyFilterSet(filters)));
|
||||
}
|
||||
|
||||
// TODO: Replace Logic
|
||||
anyFilterSet(filters: Filter[]): boolean {
|
||||
let anySelected = false;
|
||||
for (const filter of filters) {
|
||||
const options: FilterOption[] = filter.options;
|
||||
const selected = options.filter((o) => o.selected);
|
||||
if (selected?.length > 0) {
|
||||
anySelected = true;
|
||||
}
|
||||
}
|
||||
return anySelected;
|
||||
}
|
||||
ngOnInit() {}
|
||||
|
||||
getShowMainContent(animationDelayInMs: number = 500): Observable<boolean> {
|
||||
return this.filterActive$.pipe(
|
||||
|
||||
@@ -1,57 +1,27 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiFilterModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSearchboxModule } from '@ui/searchbox';
|
||||
|
||||
import { ArticleSearchComponent } from './article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './search-main/search-main.component';
|
||||
import { ArticleSearchboxComponent } from './containers/article-searchbox/article-searchbox.component';
|
||||
import { InfoboxComponent } from './containers/article-searchbox/infobox/infobox.component';
|
||||
import { ArticleSearchResultsComponent } from './search-results/search-results.component';
|
||||
import { OrderByFilterComponent } from './search-results/order-by-filter/order-by-filter.component';
|
||||
import { StockInfosPipe } from './search-results/order-by-filter/stick-infos.pipe';
|
||||
import { DomainCatalogModule } from '@domain/catalog';
|
||||
import { UiCheckboxModule } from '@ui/checkbox';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
import { UiInputModule } from '@ui/input';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { SearchResultItemComponent } from './search-results/search-result-item.component';
|
||||
import { FilterChipsModule } from './containers/filter-chips/filter-chips.module';
|
||||
import { ArticleSearchboxModule } from './containers/article-searchbox/article-searchbox.module';
|
||||
import { SearchResultsModule } from './search-results/search-results.module';
|
||||
import { SearchMainModule } from './search-main/search-main.module';
|
||||
import { SearchFilterModule } from './search-filter/search-filter.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
UiSearchboxModule,
|
||||
UiCommonModule,
|
||||
UiIconModule,
|
||||
UiFilterModule,
|
||||
UiSearchboxModule,
|
||||
ReactiveFormsModule,
|
||||
DomainCatalogModule,
|
||||
ScrollingModule,
|
||||
UiCheckboxModule,
|
||||
UiFormControlModule,
|
||||
UiInputModule,
|
||||
],
|
||||
exports: [ArticleSearchComponent, ArticleSearchMainComponent, ArticleSearchFilterComponent, ArticleSearchboxComponent, InfoboxComponent],
|
||||
declarations: [
|
||||
ArticleSearchComponent,
|
||||
ArticleSearchMainComponent,
|
||||
ArticleSearchFilterComponent,
|
||||
ArticleSearchboxComponent,
|
||||
InfoboxComponent,
|
||||
ArticleSearchResultsComponent,
|
||||
OrderByFilterComponent,
|
||||
StockInfosPipe,
|
||||
SearchResultItemComponent,
|
||||
FilterChipsModule,
|
||||
ArticleSearchboxModule,
|
||||
SearchResultsModule,
|
||||
SearchMainModule,
|
||||
SearchFilterModule,
|
||||
],
|
||||
exports: [ArticleSearchComponent],
|
||||
declarations: [ArticleSearchComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class ArticleSearchModule {}
|
||||
|
||||
@@ -1,30 +1,4 @@
|
||||
<div class="wrapper">
|
||||
<div class="primary-filter-container">
|
||||
<ng-container *ngIf="mainFilter$ | async; let mainFilter">
|
||||
<ui-checkbox
|
||||
[class.checked]="chip.options[0].selected"
|
||||
[showCheckbox]="false"
|
||||
[ngModel]="chip.options[0].selected"
|
||||
(ngModelChange)="checkMainFilter($event, chip, mainFilter)"
|
||||
[class.filter]="isFilter$ | async"
|
||||
*ngFor="let chip of mainFilter"
|
||||
>
|
||||
{{ chip.name }}
|
||||
</ui-checkbox>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="inputFilter$ | async; let inputFilter">
|
||||
<ui-checkbox
|
||||
[class.checked]="chip.options[0].selected"
|
||||
[showCheckbox]="false"
|
||||
[ngModel]="chip.options[0].selected"
|
||||
(ngModelChange)="checkInputFilter($event, chip, inputFilter)"
|
||||
[class.filter]="isFilter$ | async"
|
||||
*ngFor="let chip of inputFilter"
|
||||
>
|
||||
{{ chip.name }}
|
||||
</ui-checkbox>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div [class.searchbox-wrapper]="isFilter$ | async">
|
||||
<ui-searchbox>
|
||||
<input
|
||||
@@ -32,7 +6,7 @@
|
||||
#input
|
||||
[ngModel]="query$ | async"
|
||||
(ngModelChange)="updateQuery($event)"
|
||||
(inputChange)="autocompleteTrigger($event)"
|
||||
(inputChange)="autocompleteQuery$.next($event)"
|
||||
type="text"
|
||||
uiSearchboxInput
|
||||
placeholder="Titel, Autor, Verlag, Schlagwort, ..."
|
||||
@@ -63,7 +37,7 @@
|
||||
</ng-container>
|
||||
</button>
|
||||
|
||||
<!-- <ui-searchbox-autocomplete uiClickOutside (clicked)="autocomplete.close()" #autocomplete>
|
||||
<ui-searchbox-autocomplete uiClickOutside (clicked)="autocomplete.close()" #autocomplete>
|
||||
<button
|
||||
uiSearchboxAutocompleteOption
|
||||
[value]="result?.query"
|
||||
@@ -72,7 +46,7 @@
|
||||
>
|
||||
{{ result?.display }}
|
||||
</button>
|
||||
</ui-searchbox-autocomplete> -->
|
||||
</ui-searchbox-autocomplete>
|
||||
</ui-searchbox>
|
||||
<page-searchbox-infobox *ngIf="isFilter$ | async"></page-searchbox-infobox>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,6 @@
|
||||
@apply text-dark-cerulean border border-solid border-inactive-customer;
|
||||
}
|
||||
|
||||
ui-checkbox {
|
||||
@apply mx-px-10 px-5 py-4 text-inactive-customer font-bold text-sm whitespace-nowrap;
|
||||
border-radius: 27px;
|
||||
border: 1px solid white;
|
||||
background-color: #e9f0f8;
|
||||
}
|
||||
|
||||
ui-checkbox.filter {
|
||||
@apply bg-white;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply flex flex-col;
|
||||
|
||||
|
||||
@@ -8,18 +8,16 @@ import {
|
||||
Output,
|
||||
ViewChild,
|
||||
OnDestroy,
|
||||
ElementRef,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { AutocompleteDTO } from '@swagger/cat';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { UiSearchboxAutocompleteComponent } from '@ui/searchbox';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable } from 'rxjs';
|
||||
import { delay, first, map, shareReplay, tap, filter } from 'rxjs/operators';
|
||||
import { map, shareReplay, tap, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ArticleSearchStore } from '../../article-search-new.store';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-searchbox',
|
||||
@@ -27,21 +25,44 @@ import { ArticleSearchStore } from '../../article-search-new.store';
|
||||
styleUrls: ['article-searchbox.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class ArticleSearchboxComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(UiSearchboxAutocompleteComponent, {
|
||||
read: UiSearchboxAutocompleteComponent,
|
||||
static: false,
|
||||
})
|
||||
autocomplete: UiSearchboxAutocompleteComponent;
|
||||
autocompleteResult$: Observable<AutocompleteDTO[]>;
|
||||
readonly autocompleteQuery$ = new BehaviorSubject<string>('');
|
||||
autocompleteResult$: Observable<AutocompleteDTO[]> = combineLatest([
|
||||
this.autocompleteQuery$,
|
||||
this.store.queryTokenFilter$,
|
||||
this.store.queryParamsInputSelector$,
|
||||
]).pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
filter(([query]) => query.trim().length >= 3),
|
||||
switchMap(([query, fil, inputSelector]) =>
|
||||
this.catalog.searchComplete({
|
||||
queryToken: {
|
||||
filter: fil,
|
||||
input: query,
|
||||
take: 5,
|
||||
type: inputSelector || 'qs',
|
||||
catalogType: undefined,
|
||||
},
|
||||
})
|
||||
),
|
||||
tap((response) => {
|
||||
if (response.hits > 0) {
|
||||
this.autocomplete.open();
|
||||
} else {
|
||||
this.autocomplete.close();
|
||||
}
|
||||
}),
|
||||
map((response) => response.result)
|
||||
);
|
||||
|
||||
readonly inputFilter$ = this.articleSearchStore.inputSelectorFilter$;
|
||||
readonly mainFilter$ = this.articleSearchStore.mainFilter$;
|
||||
|
||||
readonly query$ = this.articleSearchStore.queryParamsQuery$.pipe(shareReplay());
|
||||
// readonly isHistory$ = this.articleSearchStore.query$.pipe(map((query) => query.history));
|
||||
readonly searchState$ = this.articleSearchStore.searchState$;
|
||||
// readonly autocomplete$ = this.articleSearchStore.selectAutocomplete$;
|
||||
readonly query$ = this.store.queryParamsQuery$.pipe(shareReplay());
|
||||
readonly searchState$ = this.store.searchState$;
|
||||
|
||||
isMobile: boolean;
|
||||
subscriptions = new Subscription();
|
||||
@@ -67,11 +88,6 @@ export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
isMain$ = this.mode$.pipe(
|
||||
map((type) => type === 'main'),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
isFetching$ = this.searchState$.pipe(
|
||||
map((searchState) => searchState === 'fetching'),
|
||||
shareReplay()
|
||||
@@ -85,13 +101,13 @@ export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private articleSearchStore: ArticleSearchStore,
|
||||
private nativeContainer: NativeContainerService
|
||||
private store: ArticleSearchStore,
|
||||
private nativeContainer: NativeContainerService,
|
||||
private catalog: DomainCatalogService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.detectDevice();
|
||||
this.initAutocomplete();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -100,15 +116,15 @@ export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
|
||||
startSearch() {
|
||||
const isNative = this.nativeContainer.isUiWebview().isNative;
|
||||
const query = this.articleSearchStore.query;
|
||||
const query = this.store.query;
|
||||
|
||||
if (isNative && (query?.length ?? 0) === 0) {
|
||||
return this.scan();
|
||||
} else {
|
||||
this.articleSearchStore.search({ clear: true });
|
||||
this.store.search({ clear: true });
|
||||
|
||||
this.subscriptions.add(
|
||||
this.articleSearchStore.searchState$.subscribe((state) => {
|
||||
this.store.searchState$.subscribe((state) => {
|
||||
if (state !== 'fetching' && state !== 'empty') {
|
||||
this.closeFilterOverlay.emit();
|
||||
}
|
||||
@@ -122,11 +138,11 @@ export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
.openScanner('scanBook')
|
||||
.pipe(filter((message) => message.status !== 'IN_PROGRESS'))
|
||||
.subscribe((result) => {
|
||||
this.articleSearchStore.setQuery({ query: result?.data });
|
||||
this.store.setQuery({ query: result?.data });
|
||||
|
||||
this.articleSearchStore.search({ clear: true });
|
||||
this.store.search({ clear: true });
|
||||
this.subscriptions.add(
|
||||
this.articleSearchStore.searchState$.subscribe((state) => {
|
||||
this.store.searchState$.subscribe((state) => {
|
||||
if (state !== 'fetching' && state !== 'empty') {
|
||||
this.closeFilterOverlay.emit();
|
||||
}
|
||||
@@ -139,76 +155,21 @@ export class ArticleSearchboxComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.articleSearchStore.setSearchState({ searchState: '' });
|
||||
this.articleSearchStore.setQuery({ query: '' });
|
||||
this.store.setSearchState({ searchState: '' });
|
||||
this.store.setQuery({ query: '' });
|
||||
this.autocomplete.close();
|
||||
}
|
||||
|
||||
resetSearchState() {
|
||||
this.articleSearchStore.setSearchState({ searchState: '' });
|
||||
}
|
||||
|
||||
// checkFilterChip(checked: boolean = false, changedFilterChip: Filter, filterChips: Filter[]) {
|
||||
// changedFilterChip.options[0].selected = checked;
|
||||
// if (changedFilterChip.target === 'filter') {
|
||||
// const f = filterChips.filter((fil) => fil.target === 'filter');
|
||||
// this.articleSearchStore.setInputFilter({ filter: f });
|
||||
// } else if (changedFilterChip.target === 'input') {
|
||||
|
||||
// // this.articleSearchStore.setPrimaryFilter({ filter });
|
||||
// }
|
||||
// this.cdr.markForCheck();
|
||||
// }
|
||||
|
||||
checkInputFilter(checked: boolean = false, changedFilterChip: Filter, inputFilter: Filter[]) {
|
||||
changedFilterChip.options[0].selected = checked;
|
||||
this.articleSearchStore.setInputSelectorFilter({ filter: inputFilter });
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
checkMainFilter(checked: boolean = false, changedFilterChip: Filter, mainFilter: Filter[]) {
|
||||
changedFilterChip.options[0].selected = checked;
|
||||
this.articleSearchStore.setMainFilter({ filter: mainFilter });
|
||||
this.cdr.markForCheck();
|
||||
this.store.setSearchState({ searchState: '' });
|
||||
}
|
||||
|
||||
updateQuery(query: string) {
|
||||
this.articleSearchStore.setQuery({ query });
|
||||
}
|
||||
|
||||
async autocompleteTrigger(queryAutocomplete: string) {
|
||||
// await this.query$.pipe(first()).toPromise(); // Wait for setQuery from updateQuery()
|
||||
// if (queryAutocomplete.length >= 3) {
|
||||
// this.articleSearchStore.searchAutocomplete();
|
||||
// }
|
||||
}
|
||||
|
||||
initAutocomplete() {
|
||||
// this.autocompleteResult$ = this.autocomplete$.pipe(
|
||||
// tap((results) => {
|
||||
// if (this.autocomplete) {
|
||||
// if (results.length > 0) {
|
||||
// this.autocomplete.open();
|
||||
// } else {
|
||||
// this.autocomplete.close();
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// delay(1),
|
||||
// tap(() => this.cdr.detectChanges())
|
||||
// );
|
||||
this.store.setQuery({ query });
|
||||
}
|
||||
|
||||
async detectDevice() {
|
||||
this.isMobile = await this.environmentService.isMobile();
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
focus(input: HTMLInputElement): void {
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
this.cdr.detectChanges();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiInputModule } from '@ui/input';
|
||||
import { UiSearchboxModule } from '@ui/searchbox';
|
||||
|
||||
import { ArticleSearchboxComponent } from './article-searchbox.component';
|
||||
import { InfoboxComponent } from './infobox/infobox.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, UiIconModule, UiSearchboxModule, UiInputModule],
|
||||
exports: [ArticleSearchboxComponent, InfoboxComponent],
|
||||
declarations: [ArticleSearchboxComponent, InfoboxComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class ArticleSearchboxModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="primary-filter-container">
|
||||
<ui-checkbox
|
||||
class="filter-chip-background"
|
||||
[class.checked]="chip.options[0].selected"
|
||||
[showCheckbox]="false"
|
||||
[ngModel]="chip.options[0].selected"
|
||||
(ngModelChange)="checkFilter($event, chip)"
|
||||
*ngFor="let chip of filter"
|
||||
>
|
||||
{{ chip.name }}
|
||||
</ui-checkbox>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
:host {
|
||||
--filter-chips-background-color: #e9f0f8;
|
||||
}
|
||||
|
||||
:host.filter {
|
||||
--filter-chips-background-color: #fff;
|
||||
}
|
||||
|
||||
.filter-chip-background {
|
||||
background-color: var(--filter-chips-background-color);
|
||||
}
|
||||
|
||||
.primary-filter-container {
|
||||
@apply flex flex-row justify-center mb-8;
|
||||
}
|
||||
|
||||
.checked {
|
||||
@apply text-dark-cerulean border border-solid border-inactive-customer;
|
||||
}
|
||||
|
||||
ui-checkbox {
|
||||
@apply mx-px-10 px-5 py-4 text-inactive-customer font-bold text-sm whitespace-nowrap;
|
||||
border-radius: 27px;
|
||||
border: 1px solid white;
|
||||
background-color: #e9f0f8;
|
||||
}
|
||||
|
||||
ui-checkbox.filter {
|
||||
@apply bg-white;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, Input, OnInit, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'page-filter-chips',
|
||||
templateUrl: 'filter-chips.component.html',
|
||||
styleUrls: ['filter-chips.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterChipsComponent implements OnInit {
|
||||
@Output() filterChange = new EventEmitter<Filter[]>();
|
||||
|
||||
@Input() filter: Filter[];
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
checkFilter(checked: boolean = false, changedFilterChip: Filter) {
|
||||
const filter = cloneDeep(this.filter);
|
||||
filter.find((f) => f.key === changedFilterChip.key).options[0].selected = checked;
|
||||
this.filterChange.emit(filter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiCheckboxModule } from '@ui/checkbox';
|
||||
|
||||
import { FilterChipsComponent } from './filter-chips.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, UiCheckboxModule],
|
||||
exports: [FilterChipsComponent],
|
||||
declarations: [FilterChipsComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class FilterChipsModule {}
|
||||
@@ -1,3 +1,8 @@
|
||||
<div class="filter-chips">
|
||||
<page-filter-chips class="filter" (filterChange)="checkMainFilter($event)" [filter]="mainFilter$ | async"></page-filter-chips>
|
||||
<page-filter-chips class="filter" (filterChange)="checkInputFilter($event)" [filter]="inputFilter$ | async"></page-filter-chips>
|
||||
</div>
|
||||
|
||||
<page-article-searchbox mode="filter" (closeFilterOverlay)="closeOverlay()"></page-article-searchbox>
|
||||
|
||||
<ng-container *ngIf="filters$ | async; let selectedFilters">
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
bottom: 30px;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
@apply flex flex-row justify-center;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, tap } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
|
||||
@Component({
|
||||
@@ -18,6 +18,9 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
readonly initialFilters$ = this.articleSearchStore.defaultFilter$.pipe(map((filter) => filter.filter));
|
||||
readonly searchState$ = this.articleSearchStore.searchState$;
|
||||
|
||||
readonly inputFilter$ = this.articleSearchStore.inputSelectorFilter$;
|
||||
readonly mainFilter$ = this.articleSearchStore.mainFilter$;
|
||||
|
||||
/* @internal */
|
||||
updateFilterCategory$ = new BehaviorSubject<Filter>(undefined);
|
||||
|
||||
@@ -32,23 +35,22 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
this.updateFilterCategory$.next(value);
|
||||
}
|
||||
|
||||
filterChanges: Filter[];
|
||||
initialFilter: Filter[];
|
||||
|
||||
searchStateSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private articleSearchStore: ArticleSearchStore) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.filterChanges = await this.filters$.pipe(first()).toPromise();
|
||||
ngOnInit() {
|
||||
// this.filterChanges = await this.filters$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
const filter = this.filterChanges;
|
||||
this.articleSearchStore.setFilter({ filter });
|
||||
|
||||
if (!!this.searchStateSubscription) {
|
||||
this.searchStateSubscription.unsubscribe();
|
||||
}
|
||||
// const filter = this.filterChanges;
|
||||
// this.articleSearchStore.setFilter({ filter });
|
||||
// if (!!this.searchStateSubscription) {
|
||||
// this.searchStateSubscription.unsubscribe();
|
||||
// }
|
||||
}
|
||||
|
||||
closeOverlay() {
|
||||
@@ -56,7 +58,8 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async applyFilters() {
|
||||
this.filterChanges = await this.filters$.pipe(first()).toPromise();
|
||||
// this.filterChanges = await this.filters$.pipe(first()).toPromise();
|
||||
// console.log(this.filterChanges);
|
||||
this.articleSearchStore.search({ clear: true });
|
||||
|
||||
this.searchStateSubscription = this.articleSearchStore.searchState$.subscribe((state) => {
|
||||
@@ -66,6 +69,14 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
checkInputFilter(filter: Filter[]) {
|
||||
this.articleSearchStore.setInputSelectorFilter({ filter });
|
||||
}
|
||||
|
||||
checkMainFilter(filter: Filter[]) {
|
||||
this.articleSearchStore.setMainFilter({ filter });
|
||||
}
|
||||
|
||||
updateCategory(filter: Filter) {
|
||||
this.lastSelectedFilterCategory.emit(filter);
|
||||
this.cdr.markForCheck();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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 { 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],
|
||||
exports: [ArticleSearchFilterComponent],
|
||||
declarations: [ArticleSearchFilterComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class SearchFilterModule {}
|
||||
@@ -3,16 +3,20 @@
|
||||
<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 recentQueries$ | async">
|
||||
<button (click)="setQueryHistory(recentQuery)">
|
||||
<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.input }}</p>
|
||||
<p>{{ recentQuery.friendlyName }}</p>
|
||||
</button>
|
||||
</li> -->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
@apply text-2xl mt-1 mb-px-30;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
@apply flex flex-row justify-center;
|
||||
}
|
||||
|
||||
.card-search-article {
|
||||
@apply bg-white rounded p-4 text-center;
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { first, tap } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { Filter } from '@ui/filter';
|
||||
import { NEVER } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
// import { ArticleSearchStore } from '../article-search.store';
|
||||
|
||||
@@ -11,18 +14,13 @@ import { ArticleSearchStore } from '../article-search-new.store';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
// readonly recentQueries$ = this.articleSearchStore.history$;
|
||||
readonly history$ = this.catalog.getSearchHistory({ take: 5 }).pipe(catchError(() => NEVER));
|
||||
readonly inputFilter$ = this.articleSearchStore.inputSelectorFilter$;
|
||||
readonly mainFilter$ = this.articleSearchStore.mainFilter$;
|
||||
|
||||
readonly filter$ = this.articleSearchStore.filter$;
|
||||
// readonly primaryFilter$ = this.articleSearchStore.primaryFilter$;
|
||||
|
||||
constructor(private articleSearchStore: ArticleSearchStore, private route: ActivatedRoute) {}
|
||||
constructor(private articleSearchStore: ArticleSearchStore, private catalog: DomainCatalogService, private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit() {
|
||||
// const query = this.route.snapshot.queryParams.taskCalendarSearch;
|
||||
// if (!!query) {
|
||||
// this.articleSearchStore.setTaskCalendarArticleSearchQuery({ query });
|
||||
// }
|
||||
this.articleSearchStore.connect(this.route);
|
||||
}
|
||||
|
||||
@@ -30,19 +28,15 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
this.articleSearchStore.disconnect();
|
||||
}
|
||||
|
||||
async setQueryHistory(recentQuery: { input: string; filter: string; primaryFilter: string }) {
|
||||
// const filter = await this.filter$.pipe(first()).toPromise();
|
||||
// const primaryFilter = await this.primaryFilter$.pipe(first()).toPromise();
|
||||
// this.articleSearchStore.setSelectedFilterBasedOnIdString(
|
||||
// recentQuery.filter,
|
||||
// filter.concat(primaryFilter.filter((pf) => pf.target === 'filter'))
|
||||
// );
|
||||
// if (recentQuery.primaryFilter !== 'qs') {
|
||||
// this.articleSearchStore.setSelectedFilterBasedOnIdString(
|
||||
// recentQuery.primaryFilter,
|
||||
// primaryFilter.filter((pf) => pf.target === 'input')
|
||||
// );
|
||||
// }
|
||||
// this.articleSearchStore.setQuery({ query: recentQuery.input, history: true });
|
||||
checkInputFilter(filter: Filter[]) {
|
||||
this.articleSearchStore.setInputSelectorFilter({ filter });
|
||||
}
|
||||
|
||||
checkMainFilter(filter: Filter[]) {
|
||||
this.articleSearchStore.setMainFilter({ filter });
|
||||
}
|
||||
|
||||
setQueryHistory(recentQuery: string) {
|
||||
this.articleSearchStore.setQuery({ query: recentQuery });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
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],
|
||||
exports: [ArticleSearchMainComponent],
|
||||
declarations: [ArticleSearchMainComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class SearchMainModule {}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, AfterContentInit, AfterViewInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { ScrollPositionService } from 'apps/ui/common/src/lib/scroll-position/scroll-position.service';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { first, map, withLatestFrom } from 'rxjs/operators';
|
||||
import { CacheService } from 'apps/core/cache/src/public-api';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { ArticleSearchStore } from '../article-search-new.store';
|
||||
|
||||
@Component({
|
||||
@@ -15,54 +15,90 @@ import { ArticleSearchStore } from '../article-search-new.store';
|
||||
styleUrls: ['search-results.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterContentInit, AfterViewInit {
|
||||
@ViewChild('scrollContainer', { static: false }) scrollContainer: CdkVirtualScrollViewport;
|
||||
loading$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
results$ = this.store.items$;
|
||||
|
||||
fetching$ = this.store.searchState$.pipe(map((state) => state === 'fetching'));
|
||||
|
||||
private readonly subscriptions = new Subscription();
|
||||
scrollPosSubscription: Subscription;
|
||||
|
||||
trackByItemId = (item: ItemDTO) => item.id;
|
||||
|
||||
constructor(
|
||||
private store: ArticleSearchStore,
|
||||
private route: ActivatedRoute,
|
||||
private scrollPositionService: ScrollPositionService,
|
||||
private router: Router,
|
||||
private application: ApplicationService,
|
||||
private breadcrumb: BreadcrumbService
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private cache: CacheService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.store.connect(this.route, { connected: () => this.store.search({ reload: true }) });
|
||||
this.store.connect(this.route, {
|
||||
connected: () => {
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
const cachedItems = this.cache.get(params);
|
||||
if (cachedItems) {
|
||||
this.store.setItems({ items: cachedItems });
|
||||
}
|
||||
this.store.search({ reload: true });
|
||||
|
||||
// if (params.scrollPos) {
|
||||
// this.scrollTo(Number(params.scrollPos));
|
||||
// }
|
||||
});
|
||||
},
|
||||
disconnected: () => {
|
||||
// this.store.setQueryParam({ key: 'scrollPos', value: this.scrollContainer.measureScrollOffset('top').toString() });
|
||||
this.cache.set(this.store.queryParams, this.store.items);
|
||||
},
|
||||
});
|
||||
|
||||
const { query, queryParams, hits } = this.store;
|
||||
|
||||
this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||
key: this.application.activatedProcessId,
|
||||
name: `${query} (${hits} Ergebnisse)`,
|
||||
name: `${query} (${hits ? hits : 'Lade'} Ergebnisse)`,
|
||||
path: '/product/search/results',
|
||||
params: queryParams,
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.scrollPositionService.setScrollContainerRef(this.scrollContainer);
|
||||
this.scrollPositionService.scrollToLastRememberedPositionVirtualScroll();
|
||||
// this.scrollPosSubscription = this.store.onSearch.subscribe((options) => {
|
||||
// if (options?.clear) {
|
||||
// this.store.setQueryParam({ key: 'scrollPos', value: '0' });
|
||||
// this.scrollTo(0);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.store.disconnect();
|
||||
this.loading$.complete();
|
||||
// this.subscriptions.unsubscribe();
|
||||
// this.scrollPosSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
// const scrollPos = this.route?.snapshot?.queryParams?.scrollPos;
|
||||
// if (scrollPos) {
|
||||
// this.scrollTo(Number(scrollPos));
|
||||
// }
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// const scrollPos = this.route?.snapshot?.queryParams?.scrollPos;
|
||||
// if (scrollPos) {
|
||||
// this.scrollTo(Number(scrollPos));
|
||||
// }
|
||||
}
|
||||
|
||||
// scrollTo(scrollPos: number) {
|
||||
// this.scrollContainer.scrollTo({ top: scrollPos });
|
||||
// }
|
||||
|
||||
async scrolledIndexChange(index: number) {
|
||||
// this.store.queryParamsChanges(); // Update Scrollposition
|
||||
const results = await this.results$.pipe(first()).toPromise();
|
||||
if (index >= results.length - 20 && results.length - 20 > 0) {
|
||||
this.store.search({ clear: false });
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DomainCatalogModule } from '@domain/catalog';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { OrderByFilterComponent } from './order-by-filter/order-by-filter.component';
|
||||
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
|
||||
import { SearchResultItemComponent } from './search-result-item.component';
|
||||
import { ArticleSearchResultsComponent } from './search-results.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule, DomainCatalogModule, UiCommonModule, UiIconModule, ScrollingModule],
|
||||
exports: [ArticleSearchResultsComponent, SearchResultItemComponent, OrderByFilterComponent],
|
||||
declarations: [ArticleSearchResultsComponent, SearchResultItemComponent, OrderByFilterComponent, StockInfosPipe],
|
||||
providers: [],
|
||||
})
|
||||
export class SearchResultsModule {}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { ScrollPositionService } from 'apps/ui/common/src/lib/scroll-position/scroll-position.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-catalog',
|
||||
templateUrl: 'page-catalog.component.html',
|
||||
styleUrls: ['page-catalog.component.scss'],
|
||||
providers: [ScrollPositionService],
|
||||
providers: [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PageCatalogComponent implements OnInit {
|
||||
|
||||
@@ -56,6 +56,6 @@ export class ArticleListModalComponent {
|
||||
const taskCalendarSearch: string = articles.map((article: ArticleDTO) => article.ean).join(';');
|
||||
this.modalRef.close('closeAll');
|
||||
this.moduleSwitcherService.switchToCustomer();
|
||||
this.router.navigate(['/product', 'search'], { queryParams: { taskCalendarSearch } });
|
||||
this.router.navigate(['/product', 'search', 'results'], { queryParams: { query: taskCalendarSearch } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Directive, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, Output, Renderer2 } from '@angular/core';
|
||||
import { Directive, ElementRef, EventEmitter, forwardRef, HostListener, Input, Output, Renderer2 } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Directive({
|
||||
@@ -16,7 +16,6 @@ export class UiSearchboxInputDirective implements ControlValueAccessor {
|
||||
focused = false;
|
||||
|
||||
@Input()
|
||||
@HostBinding('value')
|
||||
value: string;
|
||||
|
||||
@Output()
|
||||
@@ -77,5 +76,6 @@ export class UiSearchboxInputDirective implements ControlValueAccessor {
|
||||
this.select.emit(value);
|
||||
}
|
||||
}
|
||||
this.renderer.setProperty(this.elementRef.nativeElement, 'value', this.value);
|
||||
}
|
||||
}
|
||||
|
||||
367
package-lock.json
generated
367
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -64,12 +64,13 @@
|
||||
"intersection-observer": "^0.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ng-connection-service": "^1.0.4",
|
||||
"ngx-device-detector": "^2.0.6",
|
||||
"ng2-pdf-viewer": "^6.4.1",
|
||||
"ngx-device-detector": "^2.0.6",
|
||||
"ngx-infinite-scroll": "^7.2.0",
|
||||
"ngx-perfect-scrollbar": "^7.2.1",
|
||||
"ngx-toggle-switch": "^2.0.5",
|
||||
"node-sass": "^4.14.1",
|
||||
"object-hash": "^2.1.1",
|
||||
"rxjs": "~6.6.3",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"socket.io": "^2.2.0",
|
||||
@@ -92,6 +93,7 @@
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/object-hash": "^2.1.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"codelyzer": "^5.1.2",
|
||||
"husky": "^4.2.3",
|
||||
|
||||
@@ -230,6 +230,10 @@
|
||||
],
|
||||
"@swagger/remi": [
|
||||
"apps/swagger/remi/src/public-api.ts"
|
||||
],
|
||||
"@core/cache": [
|
||||
"dist/core/cache/core-cache",
|
||||
"dist/core/cache"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user