mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
363 lines
9.8 KiB
TypeScript
363 lines
9.8 KiB
TypeScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
|
import {ActivatedRoute, Router} from '@angular/router';
|
|
import {Keyboard} from '@capacitor/keyboard';
|
|
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
|
import {Capacitor} from '@capacitor/core';
|
|
import {
|
|
SCFacet,
|
|
SCFeatureConfiguration,
|
|
SCSearchFilter,
|
|
SCSearchQuery,
|
|
SCSearchSort,
|
|
SCThings,
|
|
} from '@openstapps/core';
|
|
import {NGXLogger} from 'ngx-logger';
|
|
import {combineLatest, Subject, Subscription} from 'rxjs';
|
|
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
|
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 {PositionService} from '../../map/position.service';
|
|
import {ConfigProvider} from '../../config/config.provider';
|
|
import {searchPageSwitchAnimation} from './search-page-switch-animation';
|
|
|
|
/**
|
|
* SearchPageComponent queries things and shows list of things as search results and filter as context menu
|
|
*/
|
|
@Component({
|
|
selector: 'stapps-search-page',
|
|
templateUrl: 'search-page.html',
|
|
styleUrls: ['search-page.scss'],
|
|
providers: [ContextMenuService],
|
|
})
|
|
export class SearchPageComponent implements OnInit, OnDestroy {
|
|
title = 'search.title';
|
|
|
|
isHebisAvailable = false;
|
|
|
|
/**
|
|
* Signalizes that the data is being loaded
|
|
*/
|
|
loading = false;
|
|
|
|
/**
|
|
* Display the navigation between default and library search
|
|
*/
|
|
@Input() showNavigation = true;
|
|
|
|
/**
|
|
* Show default data (e.g. when there is user interaction)
|
|
*/
|
|
@Input() showDefaultData = false;
|
|
|
|
/**
|
|
* Show the navigation drawer
|
|
*/
|
|
@Input() showDrawer = true;
|
|
|
|
/**
|
|
* Show "universal search" toolbar
|
|
*/
|
|
@Input() showTopToolbar = true;
|
|
|
|
/**
|
|
* Api query filter
|
|
*/
|
|
filterQuery: SCSearchFilter | undefined;
|
|
|
|
/**
|
|
* Filters the search should be initialized with
|
|
*/
|
|
@Input() forcedFilter?: SCSearchFilter;
|
|
|
|
/**
|
|
* If routing should be done if the user clicks on an item
|
|
*/
|
|
@Input() itemRouting? = true;
|
|
|
|
/**
|
|
* Thing counter to start query the next page from
|
|
*/
|
|
from = 0;
|
|
|
|
/**
|
|
* Container for queried things
|
|
*/
|
|
items: Promise<SCThings[]>;
|
|
|
|
/**
|
|
* Page size of queries
|
|
*/
|
|
pageSize = 30;
|
|
|
|
/**
|
|
* Search value from search bar
|
|
*/
|
|
queryText: string;
|
|
|
|
/**
|
|
* Emits when there is a change in the query (search, sort or filter changed)
|
|
*/
|
|
queryChanged = new Subject<void>();
|
|
|
|
/**
|
|
* Subject to handle search text changes
|
|
*/
|
|
queryTextChanged = new Subject<string>();
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
sortQuery: SCSearchSort[] | undefined;
|
|
|
|
/**
|
|
* Array of all subscriptions to Observables
|
|
*/
|
|
subscriptions: Subscription[] = [];
|
|
|
|
routeAnimation: AnimationBuilder;
|
|
|
|
/**
|
|
* Injects the providers and creates subscriptions
|
|
*
|
|
* @param alertController AlertController
|
|
* @param dataProvider DataProvider
|
|
* @param contextMenuService ContextMenuService
|
|
* @param settingsProvider SettingsProvider
|
|
* @param logger An angular logger
|
|
* @param dataRoutingService DataRoutingService
|
|
* @param router Router
|
|
* @param route ActivatedRoute
|
|
* @param positionService PositionService
|
|
* @param configProvider ConfigProvider
|
|
*/
|
|
constructor(
|
|
protected readonly alertController: AlertController,
|
|
protected dataProvider: DataProvider,
|
|
protected readonly contextMenuService: ContextMenuService,
|
|
protected readonly settingsProvider: SettingsProvider,
|
|
protected readonly logger: NGXLogger,
|
|
protected dataRoutingService: DataRoutingService,
|
|
protected router: Router,
|
|
private readonly route: ActivatedRoute,
|
|
protected positionService: PositionService,
|
|
private readonly configProvider: ConfigProvider,
|
|
animationController: AnimationController,
|
|
) {
|
|
this.routeAnimation = searchPageSwitchAnimation(animationController);
|
|
}
|
|
|
|
/**
|
|
* Fetches items with set query configuration
|
|
*
|
|
* @param append If true fetched data gets appended to existing, override otherwise (default false)
|
|
*/
|
|
protected async fetchAndUpdateItems(append = false): Promise<void> {
|
|
// build query search options
|
|
const searchOptions: SCSearchQuery = {
|
|
from: this.from,
|
|
size: this.pageSize,
|
|
};
|
|
const filters: SCSearchFilter[] = [];
|
|
|
|
if (this.queryText && this.queryText.length > 0) {
|
|
// add query string
|
|
searchOptions.query = this.queryText;
|
|
}
|
|
|
|
if (this.sortQuery) {
|
|
// add query sorting
|
|
searchOptions.sort = this.sortQuery;
|
|
}
|
|
|
|
for (const filter of [this.forcedFilter, this.filterQuery]) {
|
|
if (typeof filter !== 'undefined') {
|
|
filters.push(filter);
|
|
}
|
|
}
|
|
if (filters.length > 0) {
|
|
searchOptions.filter = {
|
|
arguments: {
|
|
filters: filters,
|
|
operation: 'and',
|
|
},
|
|
type: 'boolean',
|
|
};
|
|
}
|
|
|
|
this.loading = !append;
|
|
|
|
try {
|
|
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)();
|
|
} else {
|
|
// override items with results
|
|
this.items = (async () => {
|
|
this.updateContextFilter(result.facets);
|
|
|
|
return result.data;
|
|
})();
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hides keyboard in native app environments
|
|
*/
|
|
hideKeyboard() {
|
|
if (Capacitor.isNativePlatform()) {
|
|
Keyboard.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set starting values (e.g. forced filter, which can be set in components inheriting this one)
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
initialize() {
|
|
// nothing to do here
|
|
}
|
|
|
|
/**
|
|
* Loads next page of things
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async loadMore(): Promise<void> {
|
|
this.from += this.pageSize;
|
|
await this.fetchAndUpdateItems(true);
|
|
}
|
|
|
|
/**
|
|
* Search event of search bar
|
|
*/
|
|
searchStringChanged(queryValue: string) {
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: {query: queryValue},
|
|
queryParamsHandling: 'merge',
|
|
});
|
|
this.queryTextChanged.next(queryValue);
|
|
}
|
|
|
|
/**
|
|
* Updates the possible filter options in ContextMenuService with facets
|
|
*/
|
|
updateContextFilter(facets: SCFacet[]) {
|
|
this.contextMenuService.updateContextFilter(facets);
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.initialize();
|
|
this.contextMenuService.setContextSort({
|
|
name: 'sort',
|
|
reversed: false,
|
|
value: 'relevance',
|
|
values: [
|
|
{
|
|
reversible: false,
|
|
value: 'relevance',
|
|
},
|
|
{
|
|
reversible: true,
|
|
value: 'name',
|
|
},
|
|
{
|
|
reversible: true,
|
|
value: 'type',
|
|
},
|
|
],
|
|
});
|
|
|
|
this.subscriptions.push(
|
|
combineLatest([
|
|
this.queryTextChanged.pipe(
|
|
debounceTime(this.searchQueryDueTime),
|
|
distinctUntilChanged(),
|
|
startWith(this.queryText),
|
|
),
|
|
this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)),
|
|
this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)),
|
|
]).subscribe(async query => {
|
|
this.queryText = query[0];
|
|
this.filterQuery = query[1];
|
|
this.sortQuery = query[2];
|
|
this.from = 0;
|
|
if (typeof this.filterQuery !== 'undefined' || this.queryText?.length > 0 || this.showDefaultData) {
|
|
await this.fetchAndUpdateItems();
|
|
this.queryChanged.next();
|
|
}
|
|
}),
|
|
this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => {
|
|
if (type === 'stapps.settings.changed') {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const {category, name, value} = payload!;
|
|
this.logger.log(`received event "settings.changed" with category:
|
|
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
}
|
|
}),
|
|
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
|
if (this.itemRouting) {
|
|
void this.router.navigate(['/data-detail', item.uid]);
|
|
}
|
|
}),
|
|
);
|
|
try {
|
|
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
|
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize
|
|
*/
|
|
async ionViewWillEnter() {
|
|
const term = this.route.snapshot.queryParamMap.get('query') || undefined;
|
|
if (term) {
|
|
this.queryText = term;
|
|
this.searchStringChanged(term);
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
for (const subscription of this.subscriptions) {
|
|
subscription.unsubscribe();
|
|
}
|
|
}
|
|
}
|