From fa69625226c3b95b08251b51d254c606d6ed888c Mon Sep 17 00:00:00 2001 From: Arif Khalid <88131400+Arif-Khalid@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:11:45 +0800 Subject: [PATCH] Add filters to url (#314) Filters cannot be shared to among users. Users might want to share their current view to others, consisting of their current filters. Let's pull and store filters in the URL to allow sharing of filter combinations. --- src/app/core/services/filters.service.ts | 67 +++++++++++++++++-- src/app/core/services/label.service.ts | 4 +- .../issues-viewer/issues-viewer.component.ts | 6 +- src/app/shared/layout/header.component.ts | 5 +- 4 files changed, 71 insertions(+), 11 deletions(-) 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) => {