-
Notifications
You must be signed in to change notification settings - Fork 25
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
Add preset views #320
Add preset views #320
Changes from all commits
8f465fc
b4a7c31
bf4cd5b
ee70a06
f53a718
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<string>; | ||
}; | ||
|
||
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<string>(), | ||
deselectedLabels: new Set<string>() | ||
}; | ||
|
||
@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<Filter>(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<string>(), | ||
deselectedLabels: new Set<string>() | ||
}), | ||
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<string>(), | ||
deselectedLabels: new Set<string>() | ||
}), | ||
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<string>(['status', 'type', 'milestones', 'labels', 'deselectedLabels']); | ||
|
||
readonly defaultFilter = this.presetViews.currentlyActive; | ||
public filter$ = new BehaviorSubject<Filter>(this.defaultFilter()); | ||
// Either 'currentlyActive', 'contributions', or 'custom'. | ||
public presetView$ = new BehaviorSubject<string>('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<Filter>): void { | ||
const nextDropdownFilter: Filter = { | ||
...this.filter$.value, | ||
...newFilters | ||
}; | ||
this.filter$.next(nextDropdownFilter); | ||
this.filter$.next(this.presetViews[this.presetView$.value]()); | ||
} | ||
|
||
private updatePresetViewFromFilters(newFilter: Partial<Filter>): 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; | ||
} | ||
Comment on lines
+168
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good handling of edge cases |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
Comment on lines
+53
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Functions are well written, documented and accomplishes its task in a simple way. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,10 +12,11 @@ | |
>WATcher v{{ this.getVersion() }}</a | ||
> | ||
<span id="view-descriptor" *ngIf="auth.isAuthenticated()" style="margin-left: 10px"> | ||
({{ this.getViewDescription(viewService.currentView) }}) | ||
({{ this.presetViews[this.filtersService.presetView$.value] }}) | ||
</span> | ||
|
||
<div *ngIf="auth.isAuthenticated() && this.viewService.sessionData.sessionRepo.length > 1"> | ||
<!-- Gateway to activity dashboard, do not delete --> | ||
<!--div *ngIf="auth.isAuthenticated() && this.viewService.sessionData.sessionRepo.length > 1"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this redundant code be left in the code base? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are still unsure whether to keep the activity dashboard. Currently, this is the only gateway to the activity dashboard, so I'm afraid it will be more difficult for the next batch of ppl to unhide activity dashboard, if we ever want to develop it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. In that case could you add a comment highlighting its significance for other developers who might remove it on instinct? |
||
<button mat-button [matMenuTriggerFor]="menu"><mat-icon style="color: white">expand_more</mat-icon></button> | ||
<mat-menu #menu="matMenu"> | ||
<button | ||
|
@@ -31,6 +32,25 @@ | |
</span> | ||
</button> | ||
</mat-menu> | ||
</div--> | ||
|
||
<div *ngIf="auth.isAuthenticated()"> | ||
<button mat-button [matMenuTriggerFor]="menu"><mat-icon style="color: white">expand_more</mat-icon></button> | ||
<mat-menu #menu="matMenu"> | ||
<button | ||
mat-menu-item | ||
*ngFor="let presetView of this.presetViews | keyvalue" | ||
(click)="this.filtersService.updatePresetView(presetView.key)" | ||
> | ||
<span> | ||
<mat-icon | ||
[ngStyle]="{ color: 'green', visibility: this.filtersService.presetView$.value === presetView.key ? 'visible' : 'hidden' }" | ||
>done</mat-icon | ||
> | ||
{{ presetView.value }} | ||
</span> | ||
</button> | ||
</mat-menu> | ||
</div> | ||
|
||
<!-- everything else --> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since changing labels, hiddenLabels, deselectedLabels, sort and title do not make the preset view "custom", should we reset them or set them to a fixed value when setting the preset view?
To me, it is unintuitive that since it is "not part of the preset views" since changing these parameters did not affect the status of my preset view being put into custom, choosing a preset view should not reset these parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, is it intentional to provide currentlyActive preset with an invalid sort id of status?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. But currently, there is a regular label polling, and everytime the labels are fetched, the filters are reset, meaning that the "preset view" will be set to
Custom
after 5 seconds. I can't really think of a good way to handle this, other than having to compare whether the new labels are the same as the current filter. I would think we should not handle this in filter service, but rather, stop callingfiltersService::updateFilters
whenever the labels are fetched. Don't really want to fix that at the risk of introducing more bug at this stage.I am assuming that #318 will be merged soon, so
status
will be a valid sorting ID.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, would a better implementation be to only update certain filters when choosing a preset view then? Simply leaving labels and those other "untracked" filters as they are when switching to a certain preset view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be more inclined to standardise the preset views. The purpose of "currently active" preset view, for e.g., is to get the distribution of work between people within the current milestone. The user might forget to clear all label filters before setting the preset view to "currently active", and thereby get the wrong impression of the work distribution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see and that makes sense. Is there a reason the other filters aside from the labels are not "tracked" by the preset views?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would argue that the more important part of the preset view is to get the total count. So for the other filters not being tracked:
These are just my reasoning, let me know if you think otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your reasoning is sound. I am inclined to agree but still think there may be some confusion.
When I was testing it, I found it strange that my preset views were maintained when i did something like search by title yet my title was reset when i chose a preset view.
Perhaps a new feature could encompass making a (edited) status for the current preset view when these filters are changed while remaining in the preset view.
However, this would be in a separate PR, I have no issue with the current implementation thank you for clarifying your thought process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have found out an easy way to let the filters service track the labels fields as well. Because
sanitiseLabels
is only called whenever the labels are polled, I make sure that the update of filters from this method does not change the preset view value.