Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep filters option when switching repos #226

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app/issues-viewer/card-view/card-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GithubUser } from '../../core/models/github-user.model';
import { Issue } from '../../core/models/issue.model';
import { IssueService } from '../../core/services/issue.service';
import { FilterableComponent, FilterableSource } from '../../shared/issue-tables/filterableTypes';
import { FiltersStore } from '../../shared/issue-tables/FiltersStore';
import { IssuesDataTable } from '../../shared/issue-tables/IssuesDataTable';

@Component({
Expand Down Expand Up @@ -35,6 +36,8 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt

ngOnInit() {
this.issues = new IssuesDataTable(this.issueService, this.sort, this.paginator, this.headers, this.assignee, this.filters);
this.issues.dropdownFilter = FiltersStore.getInitialDropdownFilter();
this.issues.filter = FiltersStore.getInitialSearchFilter();
}

ngAfterViewInit(): void {
Expand Down
10 changes: 8 additions & 2 deletions src/app/shared/filter-bar/filter-bar.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<mat-grid-list cols="7" rowHeight="80px">
<mat-grid-tile colspan="3">
<mat-form-field class="search-bar">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" />
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" value="{{ this.initialSearchFilter }}" />
</mat-form-field>
</mat-grid-tile>

Expand All @@ -24,7 +24,13 @@
<mat-option value="pullrequest">Pull Request</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="standard" matSort [matSortDisableClear]="true">
<mat-form-field
appearance="standard"
matSort
[matSortActive]="this.dropdownFilter.sort"
[matSortDirection]="this.dropdownFilter.sortDirection"
[matSortDisableClear]="true"
>
<mat-label>Sort</mat-label>
<mat-select [(value)]="this.dropdownFilter.sort" (selectionChange)="applyDropdownFilter()">
<mat-option value="id">
Expand Down
34 changes: 27 additions & 7 deletions src/app/shared/filter-bar/filter-bar.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { AfterViewInit, Component, Input, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core';
import { MatSelect } from '@angular/material/select';
import { MatSort } from '@angular/material/sort';
import { MatSort, Sort } from '@angular/material/sort';
import { BehaviorSubject, Subscription } from 'rxjs';
import { LoggingService } from '../../core/services/logging.service';
import { MilestoneService } from '../../core/services/milestone.service';
import { PhaseService } from '../../core/services/phase.service';
import { DEFAULT_DROPDOWN_FILTER, DropdownFilter } from '../issue-tables/dropdownfilter';
import { DropdownFilter } from '../issue-tables/dropdownfilter';
import { FilterableComponent } from '../issue-tables/filterableTypes';
import { FiltersStore } from '../issue-tables/FiltersStore';
import { LabelFilterBarComponent } from './label-filter-bar/label-filter-bar.component';

/**
Expand All @@ -24,19 +25,25 @@ export class FilterBarComponent implements OnInit, AfterViewInit, OnDestroy {
repoChangeSubscription: Subscription;

/** Selected dropdown filter value */
dropdownFilter: DropdownFilter = DEFAULT_DROPDOWN_FILTER;
dropdownFilter: DropdownFilter = FiltersStore.getInitialDropdownFilter();

/** The initial search filter value, not updated when search filter is changed */
initialSearchFilter: string = FiltersStore.getInitialSearchFilter();

Comment on lines +28 to 32
Copy link
Collaborator

@gycgabriel gycgabriel Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why name it initial?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is named initial because this search filter variable is not updated at all when the user types. Instead this variable is simply used to initialize what is seen by the user at the search bar when they first open a new repo.

/** Selected label filters, instance passed into LabelChipBar to populate */
labelFilter$ = new BehaviorSubject<string[]>([]);
labelFilter$ = new BehaviorSubject<string[]>(FiltersStore.getInitialDropdownFilter().labels);
labelFilterSubscription: Subscription;

/** Selected label to hide */
hiddenLabels$ = new BehaviorSubject<Set<string>>(new Set());
hiddenLabels$ = new BehaviorSubject<Set<string>>(FiltersStore.getInitialDropdownFilter().hiddenLabels);
hiddenLabelSubscription: Subscription;

/** Milestone subscription */
milestoneSubscription: Subscription;

/** Sort change subscription */
sortChangeSubscription: Subscription;

/** One MatSort controls all IssueDataTables */
@ViewChild(MatSort, { static: true }) matSort: MatSort;

Expand All @@ -63,20 +70,28 @@ export class FilterBarComponent implements OnInit, AfterViewInit, OnDestroy {
this.dropdownFilter.hiddenLabels = labels;
this.applyDropdownFilter();
});

this.sortChangeSubscription = this.matSort.sortChange.subscribe((sort: Sort) => {
// No need to apply sort property as dropdownFilter.sort is already 2 way bound to the mat-select value
this.dropdownFilter.sortDirection = sort.direction;
FiltersStore.updateDropdownFilter(this.dropdownFilter);
});
}

ngOnDestroy(): void {
this.labelFilterSubscription?.unsubscribe();
this.hiddenLabelSubscription?.unsubscribe();
this.milestoneSubscription.unsubscribe();
this.repoChangeSubscription.unsubscribe();
this.sortChangeSubscription.unsubscribe();
}

/**
* Signals to IssuesDataTable that a change has occurred in filter.
* @param filterValue
*/
applyFilter(filterValue: string) {
FiltersStore.updateSearchFilter(filterValue);
this.views$?.value?.forEach((v) => (v.retrieveFilterable().filter = filterValue));
}

Expand All @@ -102,7 +117,10 @@ export class FilterBarComponent implements OnInit, AfterViewInit, OnDestroy {
* Signals to IssuesDataTable that a change has occurred in dropdown filter.
*/
applyDropdownFilter() {
this.views$?.value?.forEach((v) => (v.retrieveFilterable().dropdownFilter = this.dropdownFilter));
FiltersStore.updateDropdownFilter(this.dropdownFilter);
this.views$?.value?.forEach((v) => {
v.retrieveFilterable().dropdownFilter = this.dropdownFilter;
});
}

/**
Expand All @@ -116,10 +134,12 @@ export class FilterBarComponent implements OnInit, AfterViewInit, OnDestroy {
* Fetch and initialize all information from repository to populate Issue Dashboard.
*/
private initialize() {
// Fetch milestones and update dropdown filter
// Fetch milestones
this.milestoneSubscription = this.milestoneService.fetchMilestones().subscribe(
(response) => {
this.logger.debug('IssuesViewerComponent: Fetched milestones from Github');
// Clear previous milestones
this.dropdownFilter.milestones = [];
this.milestoneService.milestones.forEach((milestone) => this.dropdownFilter.milestones.push(milestone.number));
},
(err) => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { SimpleLabel } from '../../../core/models/label.model';
import { LabelService } from '../../../core/services/label.service';
import { LoggingService } from '../../../core/services/logging.service';
import { FiltersStore } from '../../issue-tables/FiltersStore';

@Component({
selector: 'app-label-filter-bar',
Expand Down Expand Up @@ -43,6 +44,28 @@ export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy
this.labelSubscription?.unsubscribe();
}

/** Use initial labels from the FiltersStore to populate hidden and selected labels */
useInitialLabels(): void {
const { labels: initialSelectedLabels, hiddenLabels: initialHiddenLabels } = FiltersStore.getInitialDropdownFilter();
const allLabelsSet = new Set(this.allLabels.map((simpleLabel) => simpleLabel.formattedName));

// Update hidden labels with initial labels from FiltersStore
initialHiddenLabels.forEach((hiddenLabel) => {
if (allLabelsSet.has(hiddenLabel)) {
this.hiddenLabelNames.add(hiddenLabel);
}
});
this.hiddenLabels.next(this.hiddenLabelNames);

// Update selected labels with initial labels from FiltersStore
initialSelectedLabels.forEach((selectedLabel) => {
if (allLabelsSet.has(selectedLabel)) {
this.selectedLabelNames.push(selectedLabel);
}
});
this.updateSelection();
}

hide(label: string): void {
if (this.hiddenLabelNames.has(label)) {
return;
Expand Down Expand Up @@ -80,6 +103,8 @@ export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy
this.labelSubscription = this.labelService.fetchLabels().subscribe(
(response) => {
this.logger.debug('LabelFilterBarComponent: Fetched labels from Github');
// Set timeout is used to prevent ExpressionChangedAfterItHasBeenChecked Error
setTimeout(() => this.useInitialLabels());
},
(err) => {},
() => {
Expand Down
35 changes: 35 additions & 0 deletions src/app/shared/issue-tables/FiltersStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DEFAULT_DROPDOWN_FILTER, DropdownFilter } from './dropdownfilter';

const STORED_FILTER_PROPERTIES = ['status', 'type', 'sort', 'sortDirection', 'labels', 'hiddenLabels'];

/** This static class stores the filters applied for the purpose of saving filters across repo changes */
export class FiltersStore {
/** This copy of the dropdown filter is constantly updated when a change in the drop down filter occurs */
private static _currentDropdownFilter: DropdownFilter = { ...DEFAULT_DROPDOWN_FILTER };

/** This copy of the search filter is constantly updated when a change in search filter occurs*/
private static _currentSearchFilter = '';

static updateDropdownFilter(dropdownFilter: DropdownFilter) {
for (const property of STORED_FILTER_PROPERTIES) {
this._currentDropdownFilter[property] = dropdownFilter[property];
}
}

static getInitialDropdownFilter(): DropdownFilter {
return this._currentDropdownFilter;
}

static updateSearchFilter(searchFilter: string) {
this._currentSearchFilter = searchFilter.slice();
}

static getInitialSearchFilter(): string {
return this._currentSearchFilter.slice();
}

static clearStoredFilters() {
this._currentDropdownFilter = { ...DEFAULT_DROPDOWN_FILTER };
this._currentSearchFilter = '';
}
}
1 change: 0 additions & 1 deletion src/app/shared/issue-tables/IssuesDataTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export class IssuesDataTable extends DataSource<Issue> implements FilterableSour
const displayDataChanges = [this.issueService.issues$, page, sortChange, this.filterChange, this.dropdownFilterChange].filter(
(x) => x !== undefined
);

this.issueService.startPollIssues();
this.issueSubscription = merge(...displayDataChanges)
.pipe(
Expand Down
8 changes: 6 additions & 2 deletions src/app/shared/issue-tables/dropdownfilter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { SortDirection } from '@angular/material/sort';
import { Issue } from '../../core/models/issue.model';

export type DropdownFilter = {
status: string;
type: string;
sort: string;
sortDirection: SortDirection;
labels: string[];
milestones: string[];
hiddenLabels?: Set<string>;
Expand All @@ -13,12 +15,14 @@ export const DEFAULT_DROPDOWN_FILTER = <DropdownFilter>{
status: 'all',
type: 'all',
sort: 'id',
sortDirection: 'desc',
labels: [],
milestones: []
milestones: [],
hiddenLabels: new Set()
};

/**
* This module serves to improve separation of concerns in IssuesDataTable.ts and IssueList.ts module by containing the logic for
* This function serves to improve separation of concerns in IssuesDataTable.ts and IssueList.ts module by containing the logic for
* applying dropdownFilter to the issues data table in this module.
* This module exports a single function applyDropDownFilter which is called by IssueList.
* This functions returns a function to check if a issue matches a dropdownfilter
Expand Down
1 change: 0 additions & 1 deletion src/app/shared/issue-tables/issue-sorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export function getSortedData(sort: MatSort, data: Issue[]): Issue[] {
}

const direction: number = sort.direction === 'asc' ? 1 : -1;

return data.sort((a, b) => {
switch (sort.active) {
case 'assignees':
Expand Down
12 changes: 11 additions & 1 deletion src/app/shared/layout/header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PhaseDescription, PhaseService } from '../../core/services/phase.servic
import { RepoSessionStorageService } from '../../core/services/repo-session-storage.service';
import { RepoUrlCacheService } from '../../core/services/repo-url-cache.service';
import { UserService } from '../../core/services/user.service';
import { FiltersStore } from '../issue-tables/FiltersStore';

const ISSUE_TRACKER_URL = 'https://github.com/CATcher-org/WATcher/issues';

Expand Down Expand Up @@ -242,11 +243,20 @@ export class HeaderComponent implements OnInit {
openChangeRepoDialog() {
const dialogRef = this.dialogService.openChangeRepoDialog(this.currentRepo);

/**
* dialogRef closes with [repoName, keepFilters] if confirmed, else with false
* keepFilters signals whether filters should be kept across repo changes
*/
dialogRef.afterClosed().subscribe((res) => {
if (!res) {
return;
}
const newRepo = Repo.of(res);
const newRepo = Repo.of(res[0]);
const keepFilters = res[1];

if (!keepFilters) {
FiltersStore.clearStoredFilters();
}

if (this.phaseService.isRepoSet()) {
this.changeRepositoryIfValid(newRepo, newRepo.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
.mat-dialog-actions {
justify-content: flex-end;
}

header {
display: flex;
justify-content: space-between;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<h1 mat-dialog-title class="change-repo-form-title">{{ data.repoName ? 'Change repository' : 'Select repository' }}</h1>
<header>
<h1 mat-dialog-title class="change-repo-form-title">{{ data.repoName ? 'Change repository' : 'Select repository' }}</h1>
<mat-checkbox *ngIf="data.repoName" [(ngModel)]="this.keepFilters">Keep Filters</mat-checkbox>
</header>
<div mat-dialog-content>
<form (ngSubmit)="onYesClick()">
<mat-form-field appearance="fill">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { RepoUrlCacheService } from '../../core/services/repo-url-cache.service'
})
export class RepoChangeFormComponent implements OnInit {
public repoName: String;
/** Whether or not filters should be kept when the repo changes */
public keepFilters = false;
filteredSuggestions: Observable<string[]>;
repoChangeForm = new FormControl();

Expand All @@ -30,8 +32,9 @@ export class RepoChangeFormComponent implements OnInit {
this.filteredSuggestions = this.repoUrlCacheService.getFilteredSuggestions(this.repoChangeForm);
}

/** Closes dialogRef with an array of [repoName: String, keepFilters: boolean] */
onYesClick(): void {
this.dialogRef.close(this.repoName);
this.dialogRef.close([this.repoName, this.keepFilters]);
}

onNoClick(): void {
Expand Down
Loading