diff --git a/src/app/core/models/milestone.model.ts b/src/app/core/models/milestone.model.ts index ac694595..76327344 100644 --- a/src/app/core/models/milestone.model.ts +++ b/src/app/core/models/milestone.model.ts @@ -8,10 +8,12 @@ export class Milestone implements Group { static PRWithoutMilestone: Milestone = new Milestone({ title: 'PR without a milestone', state: null }); title: string; state: string; + deadline?: Date; - constructor(milestone: { title: string; state: string }) { + constructor(milestone: { title: string; state: string; due_on?: string }) { this.title = milestone.title; this.state = milestone.state; + this.deadline = milestone.due_on ? new Date(milestone.due_on) : undefined; } public equals(other: any) { diff --git a/src/app/core/services/filters.service.ts b/src/app/core/services/filters.service.ts index 6cc30421..218b1bc7 100644 --- a/src/app/core/services/filters.service.ts +++ b/src/app/core/services/filters.service.ts @@ -3,6 +3,7 @@ import { Sort } from '@angular/material/sort'; import { BehaviorSubject, pipe } from 'rxjs'; import { SimpleLabel } from '../models/label.model'; import { Milestone } from '../models/milestone.model'; +import { MilestoneService } from './milestone.service'; export type Filter = { title: string; @@ -15,17 +16,6 @@ export type Filter = { deselectedLabels: Set; }; -export const DEFAULT_FILTER: Filter = { - title: '', - status: ['open pullrequest', 'merged pullrequest', 'open issue', 'closed issue'], - type: 'all', - sort: { active: 'id', direction: 'asc' }, - labels: [], - milestones: [], - hiddenLabels: new Set(), - deselectedLabels: new Set() -}; - @Injectable({ providedIn: 'root' }) @@ -34,13 +24,48 @@ export const DEFAULT_FILTER: Filter = { * Filters are subscribed to and emitted from this service */ export class FiltersService { - public filter$ = new BehaviorSubject(DEFAULT_FILTER); + readonly presetViews: { + [key: string]: () => Filter; + } = { + currentlyActive: () => ({ + title: '', + status: ['open pullrequest', 'merged pullrequest', 'open issue', 'closed issue'], + type: 'all', + sort: { active: 'status', direction: 'asc' }, + labels: [], + milestones: this.getMilestonesForCurrentlyActive().map((milestone) => milestone.title), + hiddenLabels: new Set(), + deselectedLabels: new Set() + }), + contributions: () => ({ + title: '', + status: ['open pullrequest', 'merged pullrequest', 'open issue', 'closed issue'], + type: 'all', + sort: { active: 'id', direction: 'desc' }, + labels: [], + milestones: this.milestoneService.milestones.map((milestone) => milestone.title), + hiddenLabels: new Set(), + deselectedLabels: new Set() + }), + custom: () => this.filter$.value + }; + + // List of keys in the new filter change that causes current filter to not qualify to be a preset view. + readonly presetChangingKeys = new Set(['status', 'type', 'milestones', 'labels', 'deselectedLabels']); + + readonly defaultFilter = this.presetViews.currentlyActive; + public filter$ = new BehaviorSubject(this.defaultFilter()); + // Either 'currentlyActive', 'contributions', or 'custom'. + public presetView$ = new BehaviorSubject('currentlyActive'); // Helps in determining whether all milestones were selected from previous repo during sanitization of milestones private previousMilestonesLength = 0; + constructor(private milestoneService: MilestoneService) {} + clearFilters(): void { - this.filter$.next(DEFAULT_FILTER); + this.filter$.next(this.defaultFilter()); + this.presetView$.next('currentlyActive'); this.previousMilestonesLength = 0; } @@ -50,6 +75,40 @@ export class FiltersService { ...newFilters }; this.filter$.next(nextDropdownFilter); + this.updatePresetViewFromFilters(newFilters); + } + + /** + * Updates the filters without updating the preset view. + * This should only be called when there are new labels/milestones. + * The preset view will be reapplied. + * @param newFilters The filters with new values + */ + private updateFiltersWithoutUpdatingPresetView(newFilters: Partial): void { + const nextDropdownFilter: Filter = { + ...this.filter$.value, + ...newFilters + }; + this.filter$.next(nextDropdownFilter); + this.filter$.next(this.presetViews[this.presetView$.value]()); + } + + private updatePresetViewFromFilters(newFilter: Partial): void { + for (const key of Object.keys(newFilter)) { + if (this.presetChangingKeys.has(key)) { + this.presetView$.next('custom'); + return; + } + } + } + + /** + * Updates the filter based on a preset view. + * @param presetViewName The name of the preset view, either 'currentlyActive', 'contributions', or 'custom'. + */ + updatePresetView(presetViewName: string) { + this.filter$.next(this.presetViews[presetViewName]()); + this.presetView$.next(presetViewName); } sanitizeLabels(allLabels: SimpleLabel[]) { @@ -71,7 +130,11 @@ export class FiltersService { const newLabels = this.filter$.value.labels.filter((label) => allLabelsSet.has(label)); - this.updateFilters({ labels: newLabels, hiddenLabels: newHiddenLabels, deselectedLabels: newDeselectedLabels }); + this.updateFiltersWithoutUpdatingPresetView({ + labels: newLabels, + hiddenLabels: newHiddenLabels, + deselectedLabels: newDeselectedLabels + }); } sanitizeMilestones(allMilestones: Milestone[]) { @@ -81,7 +144,7 @@ export class FiltersService { // All previous milestones were selected, reset to all new milestones selected if (this.filter$.value.milestones.length === this.previousMilestonesLength) { - this.updateFilters({ milestones: [...allMilestonesSet] }); + this.updateFiltersWithoutUpdatingPresetView({ milestones: [...allMilestonesSet] }); this.previousMilestonesLength = allMilestonesSet.size; return; } @@ -98,7 +161,21 @@ export class FiltersService { newMilestones.push(...allMilestonesSet); } - this.updateFilters({ milestones: newMilestones }); + this.updateFiltersWithoutUpdatingPresetView({ milestones: newMilestones }); this.previousMilestonesLength = allMilestonesSet.size; } + + getMilestonesForCurrentlyActive(): Milestone[] { + const earliestOpenMilestone = this.milestoneService.getEarliestOpenMilestone(); + if (earliestOpenMilestone) { + return [earliestOpenMilestone]; + } + + const latestClosedMilestone = this.milestoneService.getLatestClosedMilestone(); + if (latestClosedMilestone) { + return [latestClosedMilestone]; + } + + return this.milestoneService.milestones; + } } diff --git a/src/app/core/services/milestone.service.ts b/src/app/core/services/milestone.service.ts index 901f6bdd..13e7ac8a 100644 --- a/src/app/core/services/milestone.service.ts +++ b/src/app/core/services/milestone.service.ts @@ -13,7 +13,7 @@ import { GithubService } from './github.service'; * from the GitHub repository for the WATcher application. */ export class MilestoneService { - milestones: Milestone[]; + milestones: Milestone[] = []; hasNoMilestones: boolean; constructor(private githubService: GithubService) {} @@ -45,4 +45,42 @@ export class MilestoneService { return milestoneData; } + + /** + * Gets the open milestone with the earliest deadline. + * Returns null if there is no open milestone with deadline. + */ + getEarliestOpenMilestone(): Milestone { + let earliestOpenMilestone: Milestone = null; + for (const milestone of this.milestones) { + if (!milestone.deadline || milestone.state !== 'open') { + continue; + } + if (earliestOpenMilestone === null) { + earliestOpenMilestone = milestone; + } else if (milestone.deadline < earliestOpenMilestone.deadline) { + earliestOpenMilestone = milestone; + } + } + return earliestOpenMilestone; + } + + /** + * Gets the closed milestone with the latest deadline. + * Returns null if there is no closed milestone with deadline. + */ + getLatestClosedMilestone(): Milestone { + let latestClosedMilestone: Milestone = null; + for (const milestone of this.milestones) { + if (!milestone.deadline || milestone.state !== 'closed') { + continue; + } + if (latestClosedMilestone === null) { + latestClosedMilestone = milestone; + } else if (milestone.deadline > latestClosedMilestone.deadline) { + latestClosedMilestone = milestone; + } + } + return latestClosedMilestone; + } } diff --git a/src/app/issues-viewer/card-view/card-view.component.ts b/src/app/issues-viewer/card-view/card-view.component.ts index 0718cd42..a66a2a7e 100644 --- a/src/app/issues-viewer/card-view/card-view.component.ts +++ b/src/app/issues-viewer/card-view/card-view.component.ts @@ -14,6 +14,7 @@ import { MatPaginator } from '@angular/material/paginator'; import { Observable, Subscription } from 'rxjs'; import { Group } from '../../core/models/github/group.interface'; import { Issue } from '../../core/models/issue.model'; +import { FiltersService } from '../../core/services/filters.service'; import { GroupBy, GroupingContextService } from '../../core/services/grouping/grouping-context.service'; import { IssueService } from '../../core/services/issue.service'; import { FilterableComponent, FilterableSource } from '../../shared/issue-tables/filterableTypes'; @@ -52,12 +53,18 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt @Output() issueLengthChange: EventEmitter = new EventEmitter(); - constructor(public element: ElementRef, public issueService: IssueService, public groupingContextService: GroupingContextService) {} + constructor( + public element: ElementRef, + public issueService: IssueService, + public groupingContextService: GroupingContextService, + private filtersService: FiltersService + ) {} ngOnInit() { this.issues = new IssuesDataTable( this.issueService, this.groupingContextService, + this.filtersService, this.paginator, this.headers, this.group, diff --git a/src/app/shared/filter-bar/filter-bar.component.ts b/src/app/shared/filter-bar/filter-bar.component.ts index 1153c10c..94e4ca84 100644 --- a/src/app/shared/filter-bar/filter-bar.component.ts +++ b/src/app/shared/filter-bar/filter-bar.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, Input, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core'; import { MatSelect } from '@angular/material/select'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { DEFAULT_FILTER, Filter, FiltersService } from '../../core/services/filters.service'; +import { Filter, FiltersService } from '../../core/services/filters.service'; import { GroupBy, GroupingContextService } from '../../core/services/grouping/grouping-context.service'; import { LoggingService } from '../../core/services/logging.service'; import { MilestoneService } from '../../core/services/milestone.service'; @@ -24,7 +24,7 @@ export class FilterBarComponent implements OnInit, OnDestroy { repoChangeSubscription: Subscription; /** Selected dropdown filter value */ - filter: Filter = DEFAULT_FILTER; + filter: Filter = this.filtersService.defaultFilter(); groupByEnum: typeof GroupBy = GroupBy; diff --git a/src/app/shared/issue-tables/IssuesDataTable.ts b/src/app/shared/issue-tables/IssuesDataTable.ts index d5e416a7..f40f3165 100644 --- a/src/app/shared/issue-tables/IssuesDataTable.ts +++ b/src/app/shared/issue-tables/IssuesDataTable.ts @@ -4,7 +4,7 @@ import { BehaviorSubject, merge, Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { Group } from '../../core/models/github/group.interface'; import { Issue } from '../../core/models/issue.model'; -import { DEFAULT_FILTER, Filter } from '../../core/services/filters.service'; +import { Filter, FiltersService } from '../../core/services/filters.service'; import { GroupingContextService } from '../../core/services/grouping/grouping-context.service'; import { IssueService } from '../../core/services/issue.service'; import { applyDropdownFilter } from './dropdownfilter'; @@ -15,7 +15,7 @@ import { applySearchFilter } from './search-filter'; export class IssuesDataTable extends DataSource implements FilterableSource { public count = 0; - private filterChange = new BehaviorSubject(DEFAULT_FILTER); + private filterChange = new BehaviorSubject(this.filtersService.defaultFilter()); private issuesSubject = new BehaviorSubject([]); private issueSubscription: Subscription; @@ -24,6 +24,7 @@ export class IssuesDataTable extends DataSource implements FilterableSour constructor( private issueService: IssueService, private groupingContextService: GroupingContextService, + private filtersService: FiltersService, private paginator: MatPaginator, private displayedColumn: string[], private group?: Group, diff --git a/src/app/shared/layout/header.component.html b/src/app/shared/layout/header.component.html index 419e8b80..a914c022 100644 --- a/src/app/shared/layout/header.component.html +++ b/src/app/shared/layout/header.component.html @@ -12,10 +12,11 @@ >WATcher v{{ this.getVersion() }} - ({{ this.getViewDescription(viewService.currentView) }}) + ({{ this.presetViews[this.filtersService.presetView$.value] }}) -
+ + + +
+ + + +
diff --git a/src/app/shared/layout/header.component.ts b/src/app/shared/layout/header.component.ts index e1154cdc..7c0168b2 100644 --- a/src/app/shared/layout/header.component.ts +++ b/src/app/shared/layout/header.component.ts @@ -44,6 +44,14 @@ export class HeaderComponent implements OnInit { private readonly yesButtonDialogMessage = 'Yes, I wish to log out'; private readonly noButtonDialogMessage = "No, I don't wish to log out"; + readonly presetViews: { + [key: string]: string; + } = { + currentlyActive: 'Currently active', + contributions: 'Contributions', + custom: 'Custom' + }; + /** Model for the displayed repository name */ currentRepo = '';