import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subject, Subscription } from 'rxjs'; import { LoginModalService } from 'app/core/login/login-modal.service'; import { AccountService } from 'app/core/auth/account.service'; import { Account } from 'app/core/user/account.model'; import { HttpResponse } from '@angular/common/http'; import { ActivatedRoute, Router } from '@angular/router'; import { QueryParam, QueryParamBuilder, QueryParamGroup } from '@ngqp/core'; import { takeUntil } from 'rxjs/operators'; import { IFrequency } from 'app/shared/model/frequency.model'; import { IGitFilesPageDetails, GitFilesPageDetails } from 'app/shared/model/git-files-page-details.model'; import { IGitFilesAggregation } from 'app/shared/model/git-files-aggregation'; import { IGitFiles } from 'app/shared/model/git-files.model'; import { SearchInput } from 'app/shared/model/search-input.model'; import { SearchResultService } from 'app/search/search/search-result.service'; import { MetadataMessageService } from 'app/search/metadata/metadata-message.service'; import { IMetadataSelection } from 'app/search/metadata/metadata-selection.component'; @Component({ selector: 'jhi-search', templateUrl: './search.component.html', styleUrls: ['search.scss'], }) export class SearchComponent implements OnInit, OnDestroy { private static DEBOUNCE_TIME = 200; public pageSize = 4; account: Account | null = null; authSubscription?: Subscription; // Stores all search results for the current query. // Used to locally apply filters for e.g. repository and file format. // When local filters change, the results are recalculated from this variable. private queryResult?: IGitFilesPageDetails; gitFilesPageDetails?: IGitFilesPageDetails; gitFilesAggregation?: IGitFilesAggregation; private filterSelectionSubscription: Subscription = new Subscription(); private selectedRepositories = new Set<string>(); private selectedUniversities = new Set<string>(); private selectedFileFormats = new Set<string>(); public paramGroup: QueryParamGroup; private componentDestroyed$ = new Subject<void>(); public searchInput: SearchInput; constructor( protected searchResultService: SearchResultService, private accountService: AccountService, private loginModalService: LoginModalService, protected activatedRoute: ActivatedRoute, private router: Router, private qpb: QueryParamBuilder, private filterSelectionService: MetadataMessageService ) { this.searchInput = new SearchInput(); this.paramGroup = qpb.group({ searchText: qpb.stringParam('q', { debounceTime: SearchComponent.DEBOUNCE_TIME, emptyOn: '', }), page: qpb.numberParam('page', { emptyOn: 1, }), programmingLanguage: qpb.stringParam('pl', { debounceTime: SearchComponent.DEBOUNCE_TIME, emptyOn: '', }), keywords: qpb.stringParam('kw', { debounceTime: SearchComponent.DEBOUNCE_TIME, emptyOn: '', }), author: qpb.stringParam('a', { debounceTime: SearchComponent.DEBOUNCE_TIME, emptyOn: '', }), license: qpb.stringParam('l', { debounceTime: SearchComponent.DEBOUNCE_TIME, emptyOn: '', }), }); this.paramGroup.valueChanges.pipe(takeUntil(this.componentDestroyed$)).subscribe(value => { this.searchInput.setValues(value); this.search(); }); } filter(): void { if (!this.gitFilesPageDetails?.hitCount || this.gitFilesPageDetails.hitCount <= 0) { return; } this.loadPageDetails(); } search(): void { if (this.searchInput.fulltextQuery === '') { this.gitFilesAggregation = undefined; this.gitFilesPageDetails = undefined; return; } this.loadAggregation(); this.loadPageDetails(); } ngOnInit(): void { this.loadAggregation(); this.loadPageDetails(); this.authSubscription = this.accountService.getAuthenticationState().subscribe(account => (this.account = account)); this.filterSelectionSubscription.add( this.filterSelectionService.filterSelection$.subscribe(selection => this.updateSearchInfoFilter(selection)) ); } loadAggregation(): void { if (this.searchInput.fulltextQuery) { this.searchResultService .searchAggregation({ query: this.searchInput.fulltextQuery, }) .subscribe((res: HttpResponse<IGitFilesAggregation>) => { this.gitFilesAggregation = res.body || undefined; }); } } loadPageDetails(): void { if (this.searchInput.fulltextQuery) { this.searchResultService.searchPageDetails(this.searchInput, this.pageSize).subscribe((res: HttpResponse<IGitFilesPageDetails>) => { if (res.body === null) { this.queryResult = undefined; } else { this.queryResult = new GitFilesPageDetails(res.body.gitFiles, res.body.gitFiles.length); this.updatePageDetails(); } }); } } // Applies local filters to search results // and sets this.gitFilesPageDetails accordingly. private updatePageDetails(): void { if (this.queryResult !== undefined) { const matchingResults = this.queryResult.gitFiles.filter(element => this.matchesSelection(element)); if (matchingResults.length > 0) { this.gitFilesPageDetails = new GitFilesPageDetails(matchingResults, matchingResults.length); } else { this.gitFilesPageDetails = undefined; } } } // When the user updates the metadata filter selection // this function is called. // It updates the local filters. // Currently, for any category (at the moment repository and file format) // there is a set of allowed values. // If the selected item is not in the corresponding set it is added. // Otherwise it is removed. // If a set is empty the corresponding filter is not applied. // For non-empty sets, the results which will be displayed must match one of the set's elements. // (e.g. if the selectedRepositories set contains "foo" and "bar", // only results from these two repos will be shown.) public updateSearchInfoFilter(selection: IMetadataSelection): void { switch (selection.category) { case 'repository': { if (this.selectedRepositories.has(selection.value)) { this.selectedRepositories.delete(selection.value); } else { this.selectedRepositories.add(selection.value); } break; } case 'university': { // University seems to be a derived attribute. // Currently not implemented. alert('Filtering by university is not supported at the moment. Cause: University is not in the interface IGitFiles.'); /* if (this.selectedUniversities.has(selection.value)) { this.selectedUniversities.delete(selection.value); } else { this.selectedUniversities.add(selection.value); } */ break; } case 'fileFormat': { if (this.selectedFileFormats.has(selection.value)) { this.selectedFileFormats.delete(selection.value); } else { this.selectedFileFormats.add(selection.value); } break; } } this.updatePageDetails(); } // Checks if a given file matches the local filters. matchesSelection(file: IGitFiles): boolean { if (this.selectedRepositories.size > 0 && !this.selectedRepositories.has(file.repository)) { return false; } /* if (this.selectedUniversities.size > 0 && !this.selectedUniversities.has(file.university)) { return false; } */ if (this.selectedFileFormats.size > 0 && !this.selectedFileFormats.has(file.fileFormat)) { return false; } return true; } isAuthenticated(): boolean { return this.accountService.isAuthenticated(); } login(): void { this.loginModalService.open(); } ngOnDestroy(): void { if (this.authSubscription) { this.authSubscription.unsubscribe(); } if (this.filterSelectionSubscription) { this.filterSelectionSubscription.unsubscribe(); } this.componentDestroyed$.next(); this.componentDestroyed$.complete(); } onClickMe(gitFiles: IGitFiles): void { window.location.href = gitFiles.gitUrl; } public get gitFiles(): IGitFiles[] { return this.gitFilesPageDetails?.gitFiles || []; } public get repos(): IFrequency<string>[] { return this.gitFilesAggregation?.repositories || []; } public get university(): IFrequency<string>[] { return this.gitFilesAggregation?.university || []; } public get fileFormat(): IFrequency<string>[] { return this.gitFilesAggregation?.fileFormat || []; } public get pageParam(): QueryParam<number> { return this.paramGroup.get('page') as QueryParam<number>; } public onPageChange(page: number): void { this.pageParam.setValue(page); } public get hitCount(): number { const hits = this.gitFilesPageDetails?.hitCount; return hits ? hits : 0; } }