diff --git a/cypress/integration/ical.spec.ts b/cypress/integration/ical.spec.ts index f7e9ceff..1a0f5c7a 100644 --- a/cypress/integration/ical.spec.ts +++ b/cypress/integration/ical.spec.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -24,9 +24,9 @@ describe('ical', function () { }); it('should export a single event', function () { - cy.visit('/search'); - cy.get('ion-searchbar').click().type('test'); - cy.contains('ion-item', 'UNIcert (Test)').contains('ion-chip', 'Termine Auswählen').click(); + cy.visit('/search?query=test'); + cy.wait('@search'); + cy.contains('ion-chip', 'Termine Auswählen').first().click(); cy.get('ion-app > ion-modal').within(() => { cy.get('ion-footer > ion-toolbar > ion-button').should('have.attr', 'disabled'); diff --git a/src/app/modules/data/chips/action-chip-list.html b/src/app/modules/data/chips/action-chip-list.html index 6e2cadcd..42c80b7e 100644 --- a/src/app/modules/data/chips/action-chip-list.html +++ b/src/app/modules/data/chips/action-chip-list.html @@ -1,5 +1,5 @@ -
- - - -
+ + + diff --git a/src/app/modules/data/chips/action-chip-list.scss b/src/app/modules/data/chips/action-chip-list.scss index 94040a4e..18ed7893 100644 --- a/src/app/modules/data/chips/action-chip-list.scss +++ b/src/app/modules/data/chips/action-chip-list.scss @@ -1,5 +1,24 @@ -div { +/*! + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +:host { display: flex; flex-direction: row; width: fit-content; + + &:has(*) { + height: 48px; + } } diff --git a/src/app/modules/data/chips/data/add-event-action-chip.scss b/src/app/modules/data/chips/data/add-event-action-chip.scss index 9c24c2b2..bfc2a3d4 100644 --- a/src/app/modules/data/chips/data/add-event-action-chip.scss +++ b/src/app/modules/data/chips/data/add-event-action-chip.scss @@ -1,5 +1,5 @@ /*! - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. diff --git a/src/app/modules/data/data.module.ts b/src/app/modules/data/data.module.ts index 388e2e06..532d3910 100644 --- a/src/app/modules/data/data.module.ts +++ b/src/app/modules/data/data.module.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -95,6 +95,9 @@ import {BookDetailContentComponent} from './types/book/book-detail-content.compo import {BookListItemComponent} from './types/book/book-list-item.component'; import {PeriodicalListItemComponent} from './types/periodical/periodical-list-item.component'; import {PeriodicalDetailContentComponent} from './types/periodical/periodical-detail-content.component'; +import {SCThingListItemVirtualScrollStrategyDirective} from './list/sc-thing-list-item-virtual-scroll-strategy.directive'; +import {DataListItemHostDirective} from './list/data-list-item-host.directive'; +import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component'; /** * Module for handling data @@ -141,8 +144,11 @@ import {PeriodicalDetailContentComponent} from './types/periodical/periodical-de PlaceListItemComponent, PlaceMensaDetailComponent, SearchPageComponent, + SCThingListItemVirtualScrollStrategyDirective, SemesterDetailContentComponent, SemesterListItemComponent, + DataListItemHostDirective, + DataListItemHostDefaultComponent, SimpleCardComponent, SkeletonListItemComponent, SkeletonSegmentComponent, @@ -195,6 +201,7 @@ import {PeriodicalDetailContentComponent} from './types/periodical/periodical-de SettingsProvider, ], exports: [ + SCThingListItemVirtualScrollStrategyDirective, DataDetailComponent, DataDetailContentComponent, DataIconPipe, diff --git a/src/app/modules/data/list/data-list-item-host-default.component.ts b/src/app/modules/data/list/data-list-item-host-default.component.ts new file mode 100644 index 00000000..6491d88e --- /dev/null +++ b/src/app/modules/data/list/data-list-item-host-default.component.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {Component, Input} from '@angular/core'; +import {SCThings} from '@openstapps/core'; + +@Component({ + selector: 'data-list-item-host-default', + templateUrl: 'data-list-item-host-default.html', +}) +export class DataListItemHostDefaultComponent { + @Input() item: SCThings; +} diff --git a/src/app/modules/data/list/data-list-item-host-default.html b/src/app/modules/data/list/data-list-item-host-default.html new file mode 100644 index 00000000..4b00cb45 --- /dev/null +++ b/src/app/modules/data/list/data-list-item-host-default.html @@ -0,0 +1,24 @@ + + +

+ {{ 'name' | thingTranslate: item }} +

+

+ +

diff --git a/src/app/modules/data/list/data-list-item-host.directive.ts b/src/app/modules/data/list/data-list-item-host.directive.ts new file mode 100644 index 00000000..9f9ff197 --- /dev/null +++ b/src/app/modules/data/list/data-list-item-host.directive.ts @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {ComponentRef, Directive, Input, Type, ViewContainerRef} from '@angular/core'; +import {SCThings, SCThingType} from '@openstapps/core'; +import {BookListItemComponent} from '../types/book/book-list-item.component'; +import {CatalogListItemComponent} from '../types/catalog/catalog-list-item.component'; +import {DateSeriesListItemComponent} from '../types/date-series/date-series-list-item.component'; +import {EventListItemComponent} from '../types/event/event-list-item.component'; +import {FavoriteListItemComponent} from '../types/favorite/favorite-list-item.component'; +import {MessageListItemComponent} from '../types/message/message-list-item.component'; +import {OrganizationListItemComponent} from '../types/organization/organization-list-item.component'; +import {PersonListItemComponent} from '../types/person/person-list-item.component'; +import {PlaceListItemComponent} from '../types/place/place-list-item.component'; +import {SemesterListItemComponent} from '../types/semester/semester-list-item.component'; +import {VideoListItemComponent} from '../types/video/video-list-item.component'; +import {PeriodicalListItemComponent} from '../types/periodical/periodical-list-item.component'; +import {DataListItemHostDefaultComponent} from './data-list-item-host-default.component'; +import {ArticleListItemComponent} from '../types/article/article-item.component'; +import {DishListItemComponent} from '../types/dish/dish-list-item.component'; + +export interface DataListItem { + item: SCThings; +} + +const DataListItemIndex: Partial>> = { + [SCThingType.Catalog]: CatalogListItemComponent, + [SCThingType.Dish]: DishListItemComponent, + [SCThingType.DateSeries]: DateSeriesListItemComponent, + [SCThingType.AcademicEvent]: EventListItemComponent, + [SCThingType.SportCourse]: DateSeriesListItemComponent, + [SCThingType.Favorite]: FavoriteListItemComponent, + [SCThingType.Message]: MessageListItemComponent, + [SCThingType.Organization]: OrganizationListItemComponent, + [SCThingType.Person]: PersonListItemComponent, + [SCThingType.Building]: PlaceListItemComponent, + [SCThingType.Floor]: PlaceListItemComponent, + [SCThingType.PointOfInterest]: PlaceListItemComponent, + [SCThingType.Room]: PlaceListItemComponent, + [SCThingType.Semester]: SemesterListItemComponent, + [SCThingType.Video]: VideoListItemComponent, + [SCThingType.Periodical]: PeriodicalListItemComponent, + [SCThingType.Book]: BookListItemComponent, + [SCThingType.Article]: ArticleListItemComponent, +}; + +@Directive({ + selector: '[dataListItemHost]', +}) +export class DataListItemHostDirective { + private type?: Type; + + private component?: ComponentRef; + + constructor(readonly viewContainerRef: ViewContainerRef) {} + + @Input() set dataListItemHost(value: SCThings | undefined) { + if (!value) { + this.viewContainerRef.clear(); + delete this.type; + delete this.component; + return; + } + + const type = DataListItemIndex[value.type] || DataListItemHostDefaultComponent; + if (this.type !== type || !this.component) { + this.type = type; + this.viewContainerRef.clear(); + this.component = this.viewContainerRef.createComponent(this.type); + } + this.component.instance.item = value; + } +} diff --git a/src/app/modules/data/list/data-list-item.component.ts b/src/app/modules/data/list/data-list-item.component.ts index d2a926fa..27bbf8d1 100644 --- a/src/app/modules/data/list/data-list-item.component.ts +++ b/src/app/modules/data/list/data-list-item.component.ts @@ -27,7 +27,7 @@ import {DataListContext} from './data-list.component'; }) export class DataListItemComponent { /** - * Whether or not the list item should show a thumbnail + * Whether the list item should show a thumbnail */ @Input() hideThumbnail = false; @@ -40,6 +40,10 @@ export class DataListItemComponent { @Input() lines = 'inset'; + @Input() forceHeight = false; + + height?: string; + @Input() appearance: 'normal' | 'square' = 'normal'; @ContentChild(TemplateRef) contentTemplateRef: TemplateRef>; diff --git a/src/app/modules/data/list/data-list-item.html b/src/app/modules/data/list/data-list-item.html index 7cd74600..3b785215 100644 --- a/src/app/modules/data/list/data-list-item.html +++ b/src/app/modules/data/list/data-list-item.html @@ -36,82 +36,9 @@ - +
- - - - - - - - - - - - - - - - - - -
-

- {{ 'name' | thingTranslate: item }} -

-

- -

-
+ + - - - - - - + + +
diff --git a/src/app/modules/data/list/data-list.scss b/src/app/modules/data/list/data-list.scss index f6539af5..086552c5 100644 --- a/src/app/modules/data/list/data-list.scss +++ b/src/app/modules/data/list/data-list.scss @@ -1,10 +1,21 @@ +/*! + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + cdk-virtual-scroll-viewport { height: 100%; width: 100%; - - ion-list { - background: transparent; - } } ::ng-deep { diff --git a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts new file mode 100644 index 00000000..c56468f0 --- /dev/null +++ b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {Directive, forwardRef, Input, Output, ViewChild} from '@angular/core'; +import {CdkVirtualForOf, VIRTUAL_SCROLL_STRATEGY} from '@angular/cdk/scrolling'; +import {ScThingListItemVirtualScrollStrategy} from './sc-thing-list-item-virtual-scroll-strategy'; + +/** + * + */ +function factory(directive: SCThingListItemVirtualScrollStrategyDirective) { + return directive.scrollStrategy; +} + +@Directive({ + selector: 'cdk-virtual-scroll-viewport[scThingListItemVirtualScrollStrategy]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: factory, + deps: [forwardRef(() => SCThingListItemVirtualScrollStrategyDirective)], + }, + ], +}) +export class SCThingListItemVirtualScrollStrategyDirective { + scrollStrategy = new ScThingListItemVirtualScrollStrategy(); + + @ViewChild(CdkVirtualForOf) virtualForOf: CdkVirtualForOf; + + @Output() readonly loadMore = this.scrollStrategy.loadMore; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + @Input() set trackGroupBy(value: (item: any) => unknown) { + this.scrollStrategy.trackGroupBy = value; + } + + @Input() set minimumHeight(value: number) { + this.scrollStrategy.approximateItemHeight = value; + } + + @Input() set buffer(value: number) { + this.scrollStrategy.buffer = value; + } + + @Input() set gap(value: number) { + this.scrollStrategy.gap = value; + } + + @Input() set itemRenderTimeout(value: number) { + this.scrollStrategy.itemRenderTimeout = value; + } +} diff --git a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts new file mode 100644 index 00000000..76387a29 --- /dev/null +++ b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {CdkVirtualForOf, CdkVirtualScrollViewport, VirtualScrollStrategy} from '@angular/cdk/scrolling'; +import {BehaviorSubject, Subject, Subscription, takeUntil, timer} from 'rxjs'; +import {debounceTime, distinctUntilChanged, tap} from 'rxjs/operators'; +import {SCThingType} from '@openstapps/core'; + +export class ScThingListItemVirtualScrollStrategy implements VirtualScrollStrategy { + private viewport?: CdkVirtualScrollViewport; + + private virtualForOf?: CdkVirtualForOf; + + private index$ = new Subject(); + + private heights = new Map(); + + private groupHeights = new Map(); + + private offsets: number[] = []; + + private totalHeight = 0; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _items?: readonly unknown[]; + + private _groups?: readonly unknown[]; + + /** + * We use this to track loadMore + */ + private currentLength = 0; + + private dataStreamSubscription?: Subscription; + + private mutationObserver?: MutationObserver; + + approximateItemHeight = 67; + + approximateGroupSizes: Map = new Map([[SCThingType.AcademicEvent, 139]]); + + buffer = 4000; + + gap = 8; + + itemRenderTimeout = 1000; + + heightUpdateDebounceTime = 25; + + heightSetDebounceTime = 100; + + loadMore = new Subject(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + trackGroupBy: (value: any) => unknown = it => it.type; + + attach(viewport: CdkVirtualScrollViewport): void { + this.viewport = viewport; + // @ts-expect-error private property + this.virtualForOf = viewport._forOf; + this.dataStreamSubscription = this.virtualForOf?.dataStream.subscribe(items => { + this.items = items; + }); + + this.mutationObserver = new MutationObserver(() => { + const renderedItems = this.renderedItems; + if (!renderedItems) { + this.mutationObserver?.disconnect(); + return this.onPrematureDisconnect(); + } + + this.intersectionObserver?.disconnect(); + this.intersectionObserver = new IntersectionObserver(this.observeIntersection.bind(this), { + rootMargin: `${this.buffer - 64}px`, + threshold: 1, + }); + + for (const node of renderedItems) { + const [item, group] = this.getItemByNode(node, renderedItems); + + if (!this.heights.has(item)) { + this.intersectionObserver.observe(node); + } else { + node.style.height = `${this.getHeight(item, group)}px`; + } + } + }); + + const contentWrapper = this.contentWrapper; + if (!contentWrapper) return this.onPrematureDisconnect(); + this.mutationObserver.observe(contentWrapper, {childList: true, subtree: true}); + + this.setTotalContentSize(); + } + + detach(): void { + this.index$.complete(); + this.dataStreamSubscription?.unsubscribe(); + this.mutationObserver?.disconnect(); + delete this.viewport; + } + + private getHeight(item: unknown, group: unknown) { + return ( + this.heights.get(item) || + this.groupHeights.get(group) || + this.approximateGroupSizes.get(group) || + this.approximateItemHeight + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private set items(value: readonly unknown[]) { + const trackBy = this.virtualForOf?.cdkVirtualForTrackBy; + const tracks = value.map((it, i) => (trackBy ? trackBy(i, it) : it)); + if ( + this._items && + tracks.length === this._items.length && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + tracks.every((it, i) => it === this._items![i]) + ) + return; + + this._items = tracks; + this._groups = value.map(this.trackGroupBy); + this.currentLength = tracks.length; + + this.updateHeights(); + } + + scrolledIndexChange = this.index$.pipe(distinctUntilChanged()); + + private getOffsetFromIndex(index: number): number { + return this.offsets[index]; + } + + private getIndexFromOffset(offset: number): number { + return Math.max( + 0, + this.offsets.findIndex((it, i, array) => it >= offset || i === array.length - 1), + ); + } + + private updateHeights() { + if (!this._items) return; + const heights = this._items.map((it, i) => this.getHeight(it, this._groups![i]) + this.gap); + this.offsets = Array.from({length: heights.length}, () => 0); + this.totalHeight = heights.reduce((a, b, index) => (this.offsets[index + 1] = a + b), 0) + this.gap; + + this.setTotalContentSize(); + this.updateRenderedRange(); + } + + private updateRenderedRange() { + if (!this.viewport) return; + const offset = this.viewport.measureScrollOffset('top'); + const viewportSize = this.viewport.getViewportSize(); + const firstVisibleIndex = Math.max(0, this.getIndexFromOffset(offset) - 1); + const range = { + start: this.getIndexFromOffset(Math.max(0, offset - this.buffer)), + end: this.getIndexFromOffset(offset + viewportSize + this.buffer), + }; + const {start, end} = this.viewport.getRenderedRange(); + if (range.start === start && range.end === end) return; + + if (this.currentLength !== 0 && range.end === this.currentLength) { + this.currentLength++; + this.loadMore.next(); + } + + this.viewport.setRenderedRange(range); + this.viewport.setRenderedContentOffset(this.getOffsetFromIndex(range.start), 'to-start'); + + this.index$.next(firstVisibleIndex); + } + + private setTotalContentSize() { + this.viewport?.setTotalContentSize(this.totalHeight); + // @ts-expect-error TODO + this.viewport?._measureViewportSize(); + } + + observeIntersection(entries: IntersectionObserverEntry[], observer: IntersectionObserver) { + const renderedItems = this.renderedItems; + if (!renderedItems) return this.onPrematureDisconnect(); + + const update = new Subject(); + for (const entry of entries) { + if (!entry.isIntersecting) continue; + + const outerNode = entry.target as HTMLElement; + const [item] = this.getItemByNode(outerNode, renderedItems); + const node = outerNode.firstChild! as HTMLElement; + + const height = new BehaviorSubject(node.offsetHeight); + const resizeObserver = new ResizeObserver(() => { + const renderedItems = this.renderedItems; + if (!renderedItems) { + resizeObserver.disconnect(); + return this.onPrematureDisconnect(); + } + const [newItem] = this.getItemByNode(node, renderedItems); + if (newItem !== item) { + this.heights.delete(item); + resizeObserver.disconnect(); + return; + } + height.next(node.offsetHeight); + }); + resizeObserver.observe(node); + height + .pipe( + distinctUntilChanged(), + debounceTime(this.heightSetDebounceTime), + takeUntil(timer(this.itemRenderTimeout)), + tap({complete: () => resizeObserver.disconnect()}), + ) + .subscribe(height => { + this.heights.set(item, height); + outerNode.style.height = `${height}px`; + update.next(); + }); + + observer.unobserve(node); + } + update + .pipe(debounceTime(this.heightUpdateDebounceTime), takeUntil(timer(this.itemRenderTimeout))) + .subscribe(() => { + this.updateHeights(); + }); + } + + intersectionObserver?: IntersectionObserver; + + get contentWrapper() { + return this.viewport?._contentWrapper.nativeElement; + } + + get renderedItems() { + const contentWrapper = this.contentWrapper; + return contentWrapper + ? // eslint-disable-next-line unicorn/prefer-spread + (Array.from(contentWrapper.children).filter(it => it instanceof HTMLElement) as HTMLElement[]) + : undefined; + } + + getItemByNode(node: HTMLElement, renderedItems: HTMLElement[]) { + const {start} = this.viewport!.getRenderedRange(); + const index = renderedItems.indexOf(node) + start; + return [this._items![index], this._groups![index]]; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + onContentRendered(): void {} + + onContentScrolled() { + this.updateRenderedRange(); + } + + onPrematureDisconnect() { + console.warn('Virtual Scroll strategy was disconnected unexpectedly', new Error('foo').stack); + } + + onDataLengthChanged(): void { + this.setTotalContentSize(); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + onRenderedOffsetChanged(): void {} + + scrollToIndex(index: number, behavior: ScrollBehavior): void { + this.viewport?.scrollToOffset(this.getOffsetFromIndex(index), behavior); + } +} diff --git a/src/app/modules/data/list/search-page.component.ts b/src/app/modules/data/list/search-page.component.ts index 138557ca..0fb4ceb1 100644 --- a/src/app/modules/data/list/search-page.component.ts +++ b/src/app/modules/data/list/search-page.component.ts @@ -216,18 +216,20 @@ export class SearchPageComponent implements OnInit, OnDestroy { const result = await this.dataProvider.search(searchOptions); this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1; if (append) { - let items = await this.items; // append results - items = [...items, ...result.data]; - this.items = (async () => items)(); + this.items = this.items.then(it => + // fix for some very short results + it.length === result.pagination.total ? it : [...it, ...result.data], + ); } else { // override items with results - this.items = (async () => { - this.updateContextFilter(result.facets); - - return result.data; - })(); + this.updateContextFilter(result.facets); + this.items = Promise.resolve(result.data); } + + this.items.then(it => { + if (it.length === result.pagination.total) console.log('final page loaded'); + }); } catch (error) { this.logger.error(error); } finally { diff --git a/src/app/modules/favorites/favorites.service.ts b/src/app/modules/favorites/favorites.service.ts index 849d5574..24423907 100644 --- a/src/app/modules/favorites/favorites.service.ts +++ b/src/app/modules/favorites/favorites.service.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -15,15 +15,15 @@ import {Injectable} from '@angular/core'; import { SCFacet, + SCFavorite, + SCIndexableThings, + SCSaveableThing, SCSearchBooleanFilter, SCSearchFilter, SCSearchSort, SCSearchValueFilter, SCThings, - SCFavorite, - SCSaveableThing, SCThingType, - SCIndexableThings, SCUuid, } from '@openstapps/core'; import {StorageProvider} from '../storage/storage.provider'; @@ -32,7 +32,7 @@ import {ThingTranslatePipe} from '../../translation/thing-translate.pipe'; import {TranslateService} from '@ngx-translate/core'; import {ThingTranslateService} from '../../translation/thing-translate.service'; import {BehaviorSubject, Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {debounceTime, map} from 'rxjs/operators'; /** * Service handling favorites @@ -48,7 +48,8 @@ export class FavoritesService { favorites = new BehaviorSubject>(new Map()); - favoritesChanged$ = this.favorites.asObservable(); + // using debounce time 0 allows change detection to run through async suspension + favoritesChanged$ = this.favorites.pipe(debounceTime(0)); static getDataFromFavorites(items: SCFavorite[]) { return items.map(item => item.data);