mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1556: #4067 RD Artikelsuche Ergebnisliste Performance und Scrollposition Update
#4067 RD Artikelsuche Ergebnisliste Performance und Scrollposition Update
This commit is contained in:
committed by
Andreas Schickinger
parent
d86f595b1f
commit
be1a9e8f7e
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
const MATCH_TABLET = '(max-width: 1024px)';
|
||||
|
||||
@@ -23,19 +24,19 @@ export class EnvironmentService {
|
||||
return this._breakpointObserver.isMatched(MATCH_TABLET);
|
||||
}
|
||||
|
||||
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET);
|
||||
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(shareReplay());
|
||||
|
||||
matchDesktopSmall(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
|
||||
}
|
||||
|
||||
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
|
||||
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(shareReplay());
|
||||
|
||||
matchDesktop(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
|
||||
}
|
||||
|
||||
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
|
||||
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(shareReplay());
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
|
||||
|
||||
@@ -8,7 +8,6 @@ import { FocusSearchboxEvent } from './focus-searchbox.event';
|
||||
import { ArticleSearchMainAutocompleteProvider } from './providers';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
|
||||
import { isEqual } from 'lodash';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[routerLink]="closeFilterRoute"
|
||||
queryParamsHandling="preserve"
|
||||
>
|
||||
<ui-icon icon="close" size="15px"></ui-icon>
|
||||
<ui-svg-icon icon="close" [size]="25"></ui-svg-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
|
||||
<ng-container *ngIf="!mainOutletActive; else mainOutlet">
|
||||
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="flex flex-col flex-grow">
|
||||
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
import { Component, ChangeDetectionStrategy, HostBinding, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'page-search-result-item-loading',
|
||||
@@ -10,13 +7,12 @@ import { shareReplay } from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchResultItemLoadingComponent {
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
|
||||
}
|
||||
@Input()
|
||||
mainOutletActive?: boolean = false;
|
||||
|
||||
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
|
||||
constructor() {}
|
||||
|
||||
@HostBinding('style') get class() {
|
||||
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
|
||||
return this.mainOutletActive ? { height: '6.125rem' } : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<a
|
||||
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
|
||||
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
|
||||
[class.page-search-result-item__item-card-main]="mainOutletActive"
|
||||
[routerLink]="detailsPath"
|
||||
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
|
||||
[routerLinkActive]="!isTablet && !mainOutletActive ? 'active' : ''"
|
||||
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
|
||||
(click)="isDesktop ? scrollIntoView() : ''"
|
||||
>
|
||||
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
|
||||
<img
|
||||
@@ -15,10 +16,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-grid-container"
|
||||
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
|
||||
>
|
||||
<div class="page-search-result-item__item-grid-container" [class.page-search-result-item__item-grid-container-main]="mainOutletActive">
|
||||
<div
|
||||
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
|
||||
>
|
||||
@@ -67,7 +65,7 @@
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
|
||||
[class.page-search-result-item__item-price-main]="mainOutletActive$ | async"
|
||||
[class.page-search-result-item__item-price-main]="mainOutletActive"
|
||||
>
|
||||
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
|
||||
</div>
|
||||
@@ -85,7 +83,7 @@
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
|
||||
[class.justify-self-end]="!(mainOutletActive$ | async)"
|
||||
[class.justify-self-end]="!mainOutletActive"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
>
|
||||
@@ -115,9 +113,9 @@
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
[class.page-search-result-item__item-ssc-main]="mainOutletActive$ | async"
|
||||
[class.page-search-result-item__item-ssc-main]="mainOutletActive"
|
||||
>
|
||||
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive$ | async">
|
||||
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">
|
||||
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
|
||||
</div>
|
||||
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostBinding, ElementRef } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
|
||||
@@ -8,10 +8,9 @@ import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
export interface SearchResultItemComponentState {
|
||||
item?: ItemDTO;
|
||||
@@ -51,6 +50,9 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
mainOutletActive?: boolean = false;
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<ItemDTO>();
|
||||
|
||||
@@ -75,6 +77,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
get isDesktop() {
|
||||
return this._environment.matchDesktop();
|
||||
}
|
||||
|
||||
get detailsPath() {
|
||||
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
|
||||
}
|
||||
@@ -83,10 +89,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
|
||||
}
|
||||
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
|
||||
}
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
@@ -136,7 +138,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _environment: EnvironmentService,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _activatedRoute: ActivatedRoute
|
||||
private _elRef: ElementRef<HTMLElement>
|
||||
) {
|
||||
super({
|
||||
selected: false,
|
||||
@@ -144,6 +146,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
});
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
setSelected() {
|
||||
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
|
||||
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
|
||||
@@ -154,6 +160,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
}
|
||||
|
||||
@HostBinding('style') get class() {
|
||||
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
|
||||
return this.mainOutletActive ? { height: '6.125rem' } : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
#scrollContainer
|
||||
class="product-list"
|
||||
[itemSize]="(mainOutletActive$ | async) ? 98 : 187"
|
||||
minBufferPx="2800"
|
||||
maxBufferPx="2800"
|
||||
minBufferPx="935"
|
||||
maxBufferPx="1200"
|
||||
(scrolledIndexChange)="scrolledIndexChange($event)"
|
||||
>
|
||||
<search-result-item
|
||||
@@ -62,8 +62,12 @@
|
||||
(selectedChange)="addToCart($event)"
|
||||
[selectable]="isSelectable(item)"
|
||||
[item]="item"
|
||||
[mainOutletActive]="mainOutletActive$ | async"
|
||||
></search-result-item>
|
||||
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
|
||||
<page-search-result-item-loading
|
||||
[mainOutletActive]="mainOutletActive$ | async"
|
||||
*ngIf="fetching$ | async"
|
||||
></page-search-result-item-loading>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
<div *ngIf="isTablet" class="actions z-fixed">
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList, TrackByFunction } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
TrackByFunction,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
@@ -11,7 +21,7 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { CacheService } from 'apps/core/cache/src/public-api';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
|
||||
import { SearchResultItemComponent } from './search-result-item.component';
|
||||
@@ -24,7 +34,7 @@ import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/fi
|
||||
styleUrls: ['search-results.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
|
||||
@ViewChild('scrollContainer', { static: true })
|
||||
scrollContainer: CdkVirtualScrollViewport;
|
||||
@@ -58,6 +68,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
get isDesktop() {
|
||||
return this._environment.matchDesktop();
|
||||
}
|
||||
|
||||
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
|
||||
map(([filter, defaultFilter]) => {
|
||||
const filterQueryParams = filter?.getQueryParams();
|
||||
@@ -74,7 +88,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
|
||||
return this._environment.matchTablet$.pipe(map((state) => this._navigationService.mainOutletActive(this.route, state?.matches)));
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -134,7 +148,11 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
if (data.items?.length === 0) {
|
||||
this.search();
|
||||
} else {
|
||||
this.scrollTop(Number(queryParams.scroll_position ?? 0));
|
||||
if (!this.isDesktop || this._navigationService.mainOutletActive(this.route)) {
|
||||
this.scrollTop(Number(queryParams.scroll_position ?? 0));
|
||||
} else {
|
||||
this.scrollItemIntoView();
|
||||
}
|
||||
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
|
||||
for (const id of selectedItemIds) {
|
||||
if (id) {
|
||||
@@ -158,9 +176,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
const params = state.filter.getQueryParams();
|
||||
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
|
||||
const item = state.items.find((f) => f);
|
||||
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
|
||||
this._navigationService.navigateToDetails({
|
||||
processId,
|
||||
itemId: item.id,
|
||||
itemId,
|
||||
queryParams: this.isTablet ? undefined : params,
|
||||
});
|
||||
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
|
||||
@@ -174,6 +193,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.scrollItemIntoView();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions?.unsubscribe();
|
||||
|
||||
@@ -217,6 +240,13 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
setTimeout(() => this.scrollContainer.scrollTo({ top: scrollPos }), 0);
|
||||
}
|
||||
|
||||
scrollItemIntoView() {
|
||||
setTimeout(() => {
|
||||
const item = this.listItems?.find((item) => item.item.id === Number(this.route?.snapshot?.params?.id));
|
||||
item?.scrollIntoView();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async scrolledIndexChange(index: number) {
|
||||
const results = await this.results$.pipe(first()).toPromise();
|
||||
const hits = await this.hits$.pipe(first()).toPromise();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export abstract class BaseNavigationService {
|
||||
@@ -9,7 +8,6 @@ export abstract class BaseNavigationService {
|
||||
|
||||
abstract getOutletLocations(activatedRoute: ActivatedRoute): OutletLocations;
|
||||
abstract getOutletParams(activatedRoute: ActivatedRoute): OutletParams;
|
||||
abstract mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean>;
|
||||
abstract mainOutletActive(activatedRoute: ActivatedRoute): boolean;
|
||||
protected abstract _navigateTo(navigation: NavigationDetails): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { NavigationDetails, OutletLocations, OutletParams } from './defs';
|
||||
import { BaseNavigationService } from './base-navigation.service';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NavigationService extends BaseNavigationService {
|
||||
@@ -28,17 +26,9 @@ export class NavigationService extends BaseNavigationService {
|
||||
};
|
||||
}
|
||||
|
||||
mainOutletActive$(activatedRoute: ActivatedRoute): Observable<boolean> {
|
||||
return combineLatest([this._environment.matchDesktopSmall$, this._environment.matchDesktop$]).pipe(
|
||||
map(
|
||||
([desktopSmallState, desktopState]) =>
|
||||
!!this.getOutletLocations(activatedRoute)?.main && (desktopSmallState.matches || desktopState.matches)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
mainOutletActive(activatedRoute: ActivatedRoute): boolean {
|
||||
return !!this.getOutletLocations(activatedRoute)?.main && (this._environment.matchDesktopSmall() || this._environment.matchDesktop());
|
||||
mainOutletActive(activatedRoute: ActivatedRoute, isTablet?: boolean): boolean {
|
||||
const matchesTablet = isTablet ?? this._environment.matchTablet();
|
||||
return !!this.getOutletLocations(activatedRoute)?.main && !matchesTablet;
|
||||
}
|
||||
|
||||
protected async _navigateTo(navigation: NavigationDetails): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user