This is the codeAbility Sharing Platform! Learn more about the codeAbility Sharing Platform.

Skip to content
Snippets Groups Projects
  • Daniel Rainer's avatar
    Start transitioning to metadata V0.2 · 2264150f
    Daniel Rainer authored
    authors is no longer a valid keyword.
    In this commit the corresponding functionality
    is removed. It will be replaced in the future.
    
    keywords was renamed to keyword.
    Change some variable names accordingly.
    (Some changes might be unnecessary and could be reverted.
    I prefer continuing the usage of keywords,
    since it is a more logical name for a list of keywords.)
    
    programming_languages was renamed to
    programmingLanguage.
    Most corresponding variable names have not been changed.
    
    natural_languages is now called language.
    This attribute does not seem to be in use.
    2264150f
search.component.ts 8.86 KiB
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: '',
      }),
      keyword: 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;
  }
}