diff --git a/src/app/core/services/filters.service.ts b/src/app/core/services/filters.service.ts index 218b1bc77..10c486804 100644 --- a/src/app/core/services/filters.service.ts +++ b/src/app/core/services/filters.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@angular/core'; import { Sort } from '@angular/material/sort'; +import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, pipe } from 'rxjs'; import { SimpleLabel } from '../models/label.model'; import { Milestone } from '../models/milestone.model'; +import { LoggingService } from './logging.service'; import { MilestoneService } from './milestone.service'; export type Filter = { @@ -24,6 +26,7 @@ export type Filter = { * Filters are subscribed to and emitted from this service */ export class FiltersService { + public static readonly PRESET_VIEW_QUERY_PARAM_KEY = 'presetview'; readonly presetViews: { [key: string]: () => Filter; } = { @@ -61,14 +64,68 @@ export class FiltersService { // Helps in determining whether all milestones were selected from previous repo during sanitization of milestones private previousMilestonesLength = 0; - constructor(private milestoneService: MilestoneService) {} + constructor( + private logger: LoggingService, + private router: Router, + private route: ActivatedRoute, + private milestoneService: MilestoneService + ) {} + + private pushFiltersToUrl(): void { + const queryParams = {}; + for (const filterName of Object.keys(this.filter$.value)) { + if (this.filter$.value[filterName] instanceof Set) { + queryParams[filterName] = JSON.stringify([...this.filter$.value[filterName]]); + } else { + queryParams[filterName] = JSON.stringify(this.filter$.value[filterName]); + } + } + queryParams[FiltersService.PRESET_VIEW_QUERY_PARAM_KEY] = this.presetView$.value; + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + replaceUrl: true + }); + } clearFilters(): void { - this.filter$.next(this.defaultFilter()); - this.presetView$.next('currentlyActive'); + this.updatePresetView('currentlyActive'); this.previousMilestonesLength = 0; } + initializeFromURLParams() { + const nextFilter: Filter = this.defaultFilter(); + const queryParams = this.route.snapshot.queryParamMap; + try { + const presetView = queryParams.get(FiltersService.PRESET_VIEW_QUERY_PARAM_KEY); + + // Use preset view if set in url + if (presetView && this.presetViews.hasOwnProperty(presetView) && presetView !== 'custom') { + this.updatePresetView(presetView); + return; + } + + for (const filterName of Object.keys(nextFilter)) { + const stringifiedFilterData = queryParams.get(filterName); + if (!stringifiedFilterData) { + continue; + } + const filterData = JSON.parse(stringifiedFilterData); + + if (nextFilter[filterName] instanceof Set) { + nextFilter[filterName] = new Set(filterData); + } else { + nextFilter[filterName] = filterData; + } + } + this.updateFilters(nextFilter); + } catch (err) { + this.logger.info(`FiltersService: Update filters from URL failed with an error: ${err}`); + } + } + updateFilters(newFilters: Partial): void { const nextDropdownFilter: Filter = { ...this.filter$.value, @@ -76,6 +133,7 @@ export class FiltersService { }; this.filter$.next(nextDropdownFilter); this.updatePresetViewFromFilters(newFilters); + this.pushFiltersToUrl(); } /** @@ -109,9 +167,10 @@ export class FiltersService { updatePresetView(presetViewName: string) { this.filter$.next(this.presetViews[presetViewName]()); this.presetView$.next(presetViewName); + this.pushFiltersToUrl(); } - sanitizeLabels(allLabels: SimpleLabel[]) { + sanitizeLabels(allLabels: SimpleLabel[]): void { const allLabelsSet = new Set(allLabels.map((label) => label.name)); const newHiddenLabels: Set = new Set(); diff --git a/src/app/core/services/label.service.ts b/src/app/core/services/label.service.ts index edeac7d7c..cdbb6de83 100644 --- a/src/app/core/services/label.service.ts +++ b/src/app/core/services/label.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, EMPTY, Observable, of, Subscription, timer } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, of, Subject, Subscription, timer } from 'rxjs'; import { catchError, exhaustMap, finalize, map } from 'rxjs/operators'; import { Label, SimpleLabel } from '../models/label.model'; import { GithubService } from './github.service'; @@ -28,7 +28,7 @@ export class LabelService { simpleLabels: SimpleLabel[]; private labelsPollSubscription: Subscription; - private labelsSubject = new BehaviorSubject([]); + private labelsSubject = new Subject(); constructor(private githubService: GithubService) {} diff --git a/src/app/issues-viewer/issues-viewer.component.ts b/src/app/issues-viewer/issues-viewer.component.ts index 9bf8ca630..58665dda4 100644 --- a/src/app/issues-viewer/issues-viewer.component.ts +++ b/src/app/issues-viewer/issues-viewer.component.ts @@ -5,6 +5,7 @@ import { filter } from 'rxjs/operators'; import { Group } from '../core/models/github/group.interface'; import { Repo } from '../core/models/repo.model'; import { ErrorMessageService } from '../core/services/error-message.service'; +import { FiltersService } from '../core/services/filters.service'; import { GithubService } from '../core/services/github.service'; import { GroupingContextService } from '../core/services/grouping/grouping-context.service'; import { IssueService } from '../core/services/issue.service'; @@ -53,7 +54,8 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { public labelService: LabelService, public milestoneService: MilestoneService, public groupingContextService: GroupingContextService, - private router: Router + private router: Router, + private filtersService: FiltersService ) { this.repoChangeSubscription = this.viewService.repoChanged$.subscribe((newRepo) => { this.issueService.reset(false); @@ -75,6 +77,7 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { if (event instanceof NavigationEnd && event.id === this.popStateNavigationId) { this.viewService.initializeRepoFromUrlParams(); this.groupingContextService.initializeFromUrlParams(); + this.filtersService.initializeFromURLParams(); } }); } @@ -82,6 +85,7 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit() { this.initialize(); this.groupingContextService.initializeFromUrlParams(); + this.filtersService.initializeFromURLParams(); } ngAfterViewInit(): void { diff --git a/src/app/shared/layout/header.component.ts b/src/app/shared/layout/header.component.ts index ef872d368..441a08f68 100644 --- a/src/app/shared/layout/header.component.ts +++ b/src/app/shared/layout/header.component.ts @@ -249,10 +249,6 @@ export class HeaderComponent implements OnInit { return; } - if (!keepFilters) { - this.filtersService.clearFilters(); - } - this.viewService .changeRepositoryIfValid(repo) .then(() => { @@ -260,6 +256,7 @@ export class HeaderComponent implements OnInit { this.currentRepo = newRepoString; if (!keepFilters) { this.groupingContextService.reset(); + this.filtersService.clearFilters(); } }) .catch((error) => {