-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{'name' | thingTranslate: item}}
-
-
-
-
{{'type' | thingTranslate: item}}
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{'name' | thingTranslate: item}}
+
+
+
+
diff --git a/src/app/modules/data/list/data-list-item.scss b/src/app/modules/data/list/data-list-item.scss
index e69de29b..75297d19 100644
--- a/src/app/modules/data/list/data-list-item.scss
+++ b/src/app/modules/data/list/data-list-item.scss
@@ -0,0 +1,8 @@
+
+::ng-deep {
+ ion-grid,ion-col {
+ padding-inline-start: 0px!important;
+ padding-top: 0px!important;
+ padding-bottom: 0px!important;
+ }
+}
diff --git a/src/app/modules/data/list/data-list.component.spec.ts b/src/app/modules/data/list/data-list.component.spec.ts
index 9cb68a4c..9f4ec078 100644
--- a/src/app/modules/data/list/data-list.component.spec.ts
+++ b/src/app/modules/data/list/data-list.component.spec.ts
@@ -1,6 +1,7 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DataListComponent} from './data-list.component';
+import {TranslateModule} from '@ngx-translate/core';
describe('DataListComponent', () => {
let component: DataListComponent;
@@ -8,7 +9,10 @@ describe('DataListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [ DataListComponent ]
+ declarations: [ DataListComponent ],
+ imports: [
+ TranslateModule.forRoot(),
+ ]
})
.compileComponents();
}));
diff --git a/src/app/modules/data/list/data-list.component.ts b/src/app/modules/data/list/data-list.component.ts
index 69a750a4..e79a34ee 100644
--- a/src/app/modules/data/list/data-list.component.ts
+++ b/src/app/modules/data/list/data-list.component.ts
@@ -12,8 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see
.
*/
-import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
import {SCThings} from '@openstapps/core';
+import {BehaviorSubject, Observable, Subscription} from 'rxjs';
+
/**
* Shows the list of items
*/
@@ -22,16 +25,84 @@ import {SCThings} from '@openstapps/core';
templateUrl: 'data-list.html',
styleUrls: ['data-list.scss'],
})
-export class DataListComponent implements OnInit {
- // tslint:disable-next-line:completed-docs
- @Input() items: SCThings[];
- // tslint:disable-next-line:completed-docs
- @Output('loadmore') loadMore = new EventEmitter
();
- ngOnInit(): void {
+export class DataListComponent implements OnChanges, OnInit {
+ /**
+ * Amount of list items left to show (in percent) that should trigger a data reload
+ */
+ private readonly reloadThreshold = 0.2;
+ /**
+ * All SCThings to display
+ */
+ @Input() items?: SCThings[];
+ /**
+ * Stream of SCThings for virtual scroll to consume
+ */
+ itemStream = new BehaviorSubject([]);
+ /**
+ * Output binding to trigger pagination fetch
+ */
+ @Output('loadmore') loadMore = new EventEmitter();
+ /**
+ * Indicates whether or not the list is to display SCThings of a single type
+ */
+ @Input() singleType = false;
+ // tslint:disable-next-line: completed-docs
+ @ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
+
+ /**
+ * Uniquely identifies item at a certain list index
+ */
+ identifyItem(_index: number, item: SCThings) {
+ return item.uid;
}
- notifyLoadMore(e: Event) {
- this.loadMore.emit(e);
+ // tslint:disable-next-line: completed-docs
+ ngOnChanges(_changes: SimpleChanges): void {
+ if (Array.isArray(this.items)) {
+ if (this.itemStream.getValue().length === 0) {
+ this.itemStream = new BehaviorSubject(this.items);
+ } else {
+ if ((this.items[0].uid ?? '') !== this.itemStream.getValue()[0].uid) {
+ this.itemStream = new BehaviorSubject(this.items);
+ }
+ }
+ }
+ }
+
+ // tslint:disable-next-line: completed-docs
+ ngOnInit(): void {
+ if (typeof this.queryChanged !== 'undefined') {
+ this.subscriptions.push(this.queryChanged.subscribe(() => {
+ this.viewPort.scrollToIndex(0);
+ }));
+ }
+ }
+
+ // tslint:disable-next-line: completed-docs
+ ngOnDestroy(): void {
+ for (const subscription of this.subscriptions) {
+ subscription.unsubscribe();
+ }
+ }
+
+ /**
+ * Component proxy for dataSource.finishedLoadMore
+ */
+ notifyLoadMore() {
+ this.loadMore.emit((items) => {
+ this.itemStream.next(items);
+ });
+ }
+
+ /**
+ * Function to call whenever scroll view visible range changed
+ */
+ scrolled(_event: Event) {
+ if ((this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <= (this.items?.length ?? 0) * this.reloadThreshold) {
+ this.notifyLoadMore();
+ }
}
}
+
+export type DataSourceRefreshed = (items: SCThings[]) => unknown;
diff --git a/src/app/modules/data/list/data-list.html b/src/app/modules/data/list/data-list.html
index 5d5ccac9..de8a63d0 100644
--- a/src/app/modules/data/list/data-list.html
+++ b/src/app/modules/data/list/data-list.html
@@ -1,20 +1,15 @@
-
-
-
0; else empty'>
-
-
-
-
-
- {{'search.nothing_found' | translate}}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {{'search.nothing_found' | translate | titlecase}}
+
+
+
+
+
diff --git a/src/app/modules/data/list/data-list.scss b/src/app/modules/data/list/data-list.scss
index e69de29b..3965e303 100644
--- a/src/app/modules/data/list/data-list.scss
+++ b/src/app/modules/data/list/data-list.scss
@@ -0,0 +1,10 @@
+cdk-virtual-scroll-viewport {
+ min-height: 100%;
+ width: 100%;
+}
+
+::ng-deep {
+ .cdk-virtual-scroll-content-wrapper {
+ width: 100%;
+ }
+}
diff --git a/src/app/modules/data/list/search-page.component.ts b/src/app/modules/data/list/search-page.component.ts
index add45892..823ad0e9 100644
--- a/src/app/modules/data/list/search-page.component.ts
+++ b/src/app/modules/data/list/search-page.component.ts
@@ -29,6 +29,7 @@ import {ContextMenuService} from '../../menu/context/context-menu.service';
import {SettingsProvider} from '../../settings/settings.provider';
import {DataRoutingService} from '../data-routing.service';
import {DataProvider} from '../data.provider';
+import {DataSourceRefreshed} from './data-list.component';
/**
* SearchPageComponent queries things and shows list of things as search results and filter as context menu
@@ -71,6 +72,10 @@ export class SearchPageComponent implements OnInit {
* Time to wait for search query if search text is changing
*/
searchQueryDueTime = 1000;
+ /**
+ * Search response only ever contains a single SCThingType
+ */
+ singleTypeResponse = false;
/**
* Api query sorting
*/
@@ -183,6 +188,7 @@ export class SearchPageComponent implements OnInit {
return this.dataProvider.search(searchOptions)
.then(async (result) => {
+ this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
if (append) {
let items = await this.items;
// append results
@@ -219,10 +225,10 @@ export class SearchPageComponent implements OnInit {
* Loads next page of things
*/
// tslint:disable-next-line:no-any
- async loadMore(event: any): Promise {
+ async loadMore(finished: DataSourceRefreshed): Promise {
this.from += this.pageSize;
await this.fetchAndUpdateItems(true);
- event.target.complete();
+ finished(await this.items);
}
/**
diff --git a/src/app/modules/data/list/search-page.html b/src/app/modules/data/list/search-page.html
index 0ccf08d6..22172e2b 100644
--- a/src/app/modules/data/list/search-page.html
+++ b/src/app/modules/data/list/search-page.html
@@ -1,4 +1,4 @@
-
+
@@ -15,5 +15,5 @@
-
+