Skip to content

Commit

Permalink
Merge branch 'main' into preset-view
Browse files Browse the repository at this point in the history
  • Loading branch information
nknguyenhc authored Mar 29, 2024
2 parents ee70a06 + b1f3eed commit f53a718
Show file tree
Hide file tree
Showing 19 changed files with 219 additions and 34 deletions.
7 changes: 5 additions & 2 deletions src/app/core/models/issue.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ export class Issue {
this.title = githubIssue.title;
this.hiddenDataInDescription = new HiddenData(githubIssue.body);
this.description = Issue.updateDescription(this.hiddenDataInDescription.originalStringWithoutHiddenData);
// githubIssue without milestone will be set to default milestone
this.milestone = githubIssue.milestone ? new Milestone(githubIssue.milestone) : Milestone.DefaultMilestone;
this.state = githubIssue.state;
this.stateReason = githubIssue.stateReason;
this.issueOrPr = githubIssue.issueOrPr;
Expand All @@ -94,6 +92,11 @@ export class Issue {
this.assignees = githubIssue.assignees.map((assignee) => assignee.login);
this.githubLabels = githubIssue.labels;
this.labels = githubIssue.labels.map((label) => label.name);
this.milestone = githubIssue.milestone
? new Milestone(githubIssue.milestone)
: this.issueOrPr === 'Issue'
? Milestone.IssueWithoutMilestone
: Milestone.PRWithoutMilestone;
}

public static createPhaseBugReportingIssue(githubIssue: GithubIssue): Issue {
Expand Down
14 changes: 10 additions & 4 deletions src/app/core/models/milestone.model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Group } from './github/group.interface';

/**
* Represents a milestone and its attributes fetched from Github.
*/
export class Milestone {
static DefaultMilestone: Milestone = new Milestone({ title: 'Without a milestone', state: null });
export class Milestone implements Group {
static IssueWithoutMilestone: Milestone = new Milestone({ title: 'Issue without a milestone', state: null });
static PRWithoutMilestone: Milestone = new Milestone({ title: 'PR without a milestone', state: null });
title: string;
state: string;
deadline?: Date;
Expand All @@ -13,7 +16,10 @@ export class Milestone {
this.deadline = milestone.due_on ? new Date(milestone.due_on) : undefined;
}

public equals(milestone: Milestone) {
return this.title === milestone.title;
public equals(other: any) {
if (!(other instanceof Milestone)) {
return false;
}
return this.title === other.title;
}
}
8 changes: 5 additions & 3 deletions src/app/core/services/filters.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,14 @@ export class FiltersService {
}

sanitizeMilestones(allMilestones: Milestone[]) {
const allMilestonesSet = new Set(allMilestones.map((milestone) => milestone.title));
const milestones = allMilestones.map((milestone) => milestone.title);
milestones.push(Milestone.IssueWithoutMilestone.title, Milestone.PRWithoutMilestone.title);
const allMilestonesSet = new Set(milestones);

// All previous milestones were selected, reset to all new milestones selected
if (this.filter$.value.milestones.length === this.previousMilestonesLength) {
this.updateFiltersWithoutUpdatingPresetView({ milestones: [...allMilestonesSet] });
this.previousMilestonesLength = allMilestones.length;
this.previousMilestonesLength = allMilestonesSet.size;
return;
}

Expand All @@ -160,7 +162,7 @@ export class FiltersService {
}

this.updateFiltersWithoutUpdatingPresetView({ milestones: newMilestones });
this.previousMilestonesLength = allMilestones.length;
this.previousMilestonesLength = allMilestonesSet.size;
}

getMilestonesForCurrentlyActive(): Milestone[] {
Expand Down
35 changes: 31 additions & 4 deletions src/app/core/services/grouping/grouping-context.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Injectable, Injector } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { Group } from '../../models/github/group.interface';
import { Issue } from '../../models/issue.model';
import { AssigneeGroupingStrategy } from './assignee-grouping-strategy.service';
import { GroupingStrategy } from './grouping-strategy.interface';
import { MilestoneGroupingStrategy } from './milestone-grouping-strategy.service';

export enum GroupBy {
Assignee = 'assignee'
Assignee = 'assignee',
Milestone = 'milestone'
}

export const DEFAULT_GROUPBY = GroupBy.Assignee;
Expand All @@ -18,13 +21,14 @@ export const DEFAULT_GROUPBY = GroupBy.Assignee;
providedIn: 'root'
})
export class GroupingContextService {
public static readonly GROUP_BY_QUERY_PARAM_KEY = 'groupby';
private currGroupBySubject: BehaviorSubject<GroupBy>;
currGroupBy: GroupBy;
currGroupBy$: Observable<GroupBy>;

private groupingStrategyMap: Map<string, GroupingStrategy>;

constructor(private injector: Injector) {
constructor(private injector: Injector, private route: ActivatedRoute, private router: Router) {
this.currGroupBy = DEFAULT_GROUPBY;
this.currGroupBySubject = new BehaviorSubject<GroupBy>(this.currGroupBy);
this.currGroupBy$ = this.currGroupBySubject.asObservable();
Expand All @@ -33,15 +37,38 @@ export class GroupingContextService {

// Initialize the grouping strategy map with available strategies
this.groupingStrategyMap.set(GroupBy.Assignee, this.injector.get(AssigneeGroupingStrategy));
this.groupingStrategyMap.set(GroupBy.Milestone, this.injector.get(MilestoneGroupingStrategy));
}

/**
* Sets the current grouping type.
* @param groupBy - The grouping type to set.
* Initializes the service from URL parameters.
*/
initializeFromUrlParams() {
const groupByParam = this.route.snapshot.queryParamMap.get(GroupingContextService.GROUP_BY_QUERY_PARAM_KEY);

if (groupByParam && Object.values(GroupBy).includes(groupByParam as GroupBy)) {
this.setCurrentGroupingType(groupByParam as GroupBy);
} else {
this.setCurrentGroupingType(DEFAULT_GROUPBY);
}
}

/**
* Sets the current grouping type and updates the corresponding query parameter in the URL.
* @param groupBy The grouping type to set.
*/
setCurrentGroupingType(groupBy: GroupBy): void {
this.currGroupBy = groupBy;
this.currGroupBySubject.next(this.currGroupBy);

this.router.navigate([], {
relativeTo: this.route,
queryParams: {
[GroupingContextService.GROUP_BY_QUERY_PARAM_KEY]: groupBy
},
queryParamsHandling: 'merge',
replaceUrl: true
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Issue } from '../../models/issue.model';
import { Milestone } from '../../models/milestone.model';
import { MilestoneService } from '../milestone.service';
import { GroupingStrategy } from './grouping-strategy.interface';

/**
* A GroupingStrategy that groups issues/prs based on their milestones.
*/
@Injectable({
providedIn: 'root'
})
export class MilestoneGroupingStrategy implements GroupingStrategy {
constructor(private milestoneService: MilestoneService) {}

/**
* Retrieves data for a milestone.
*/
getDataForGroup(issues: Issue[], key: Milestone): Issue[] {
return issues.filter((issue) => issue.milestone.equals(key));
}

/**
* Retrieves an Observable emitting milestones available for grouping issues.
*/
getGroups(): Observable<Milestone[]> {
return this.milestoneService.fetchMilestones().pipe(
map((milestones) => {
return this.milestoneService.parseMilestoneData(milestones);
})
);
}

/**
* Groups other than Default Milestone need to be shown on the
* hidden group list if empty.
*/
isInHiddenList(group: Milestone): boolean {
return group !== Milestone.IssueWithoutMilestone && group !== Milestone.PRWithoutMilestone;
}
}
2 changes: 0 additions & 2 deletions src/app/core/services/milestone.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ export class MilestoneService {
}
milestoneData.sort((a: Milestone, b: Milestone) => a.title.localeCompare(b.title));

// add default milestone for untracked issues/PRs at the end
milestoneData.push(Milestone.DefaultMilestone);
return milestoneData;
}

Expand Down
3 changes: 2 additions & 1 deletion src/app/core/services/view.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export class ViewService {
this.router.navigate(['issuesViewer'], {
queryParams: {
[ViewService.REPO_QUERY_PARAM_KEY]: repo.toString()
}
},
queryParamsHandling: 'merge'
});
}

Expand Down
13 changes: 13 additions & 0 deletions src/app/issues-viewer/card-view/card-view.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@
</mat-card>
</div>
</ng-template>

<ng-template #milestoneHeader let-milestone>
<div class="column-header">
<mat-card>
<mat-card-header [ngStyle]="{ height: '40px' }">
<mat-card-title>
{{ milestone.title }}
</mat-card-title>
<div class="row-count">{{ this.issues.count }}</div>
</mat-card-header>
</mat-card>
</div>
</ng-template>
31 changes: 25 additions & 6 deletions src/app/issues-viewer/card-view/card-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ViewChild
} from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { Observable } from 'rxjs';
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';
Expand All @@ -37,10 +37,15 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild('defaultHeader') defaultHeaderTemplate: TemplateRef<any>;
@ViewChild('assigneeHeader') assigneeHeaderTemplate: TemplateRef<any>;
@ViewChild('milestoneHeader') milestoneHeaderTemplate: TemplateRef<any>;

issues: IssuesDataTable;
issues$: Observable<Issue[]>;

private timeoutId: NodeJS.Timeout | null = null;
private issuesLengthSubscription: Subscription;
private issuesLoadingStateSubscription: Subscription;

isLoading = true;
issueLength = 0;

Expand Down Expand Up @@ -68,18 +73,18 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt
}

ngAfterViewInit(): void {
setTimeout(() => {
this.timeoutId = setTimeout(() => {
this.issues.loadIssues();
this.issues$ = this.issues.connect();

// Emit event when issues change
this.issues$.subscribe(() => {
this.issuesLengthSubscription = this.issues$.subscribe(() => {
this.issueLength = this.issues.count;
this.issueLengthChange.emit(this.issueLength);
});

// Emit event when loading state changes
this.issues.isLoading$.subscribe((isLoadingUpdate) => {
this.issuesLoadingStateSubscription = this.issues.isLoading$.subscribe((isLoadingUpdate) => {
this.isLoading = isLoadingUpdate;
});
});
Expand All @@ -89,15 +94,29 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt
switch (this.groupingContextService.currGroupBy) {
case GroupBy.Assignee:
return this.assigneeHeaderTemplate;
case GroupBy.Milestone:
return this.milestoneHeaderTemplate;
default:
return this.defaultHeaderTemplate;
}
}

ngOnDestroy(): void {
setTimeout(() => {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}

if (this.issues) {
this.issues.disconnect();
});
}

if (this.issuesLengthSubscription) {
this.issuesLengthSubscription.unsubscribe();
}

if (this.issuesLoadingStateSubscription) {
this.issuesLoadingStateSubscription.unsubscribe();
}
}

retrieveFilterable(): FilterableSource {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@
</mar-card-header>
</mat-card>
</ng-template>

<ng-template #milestoneCard let-milestone>
<mat-card>
<mar-card-header class="mat-card-header">
<mat-card-title>{{ milestone.title }}</mat-card-title>
</mar-card-header>
</mat-card>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ export class HiddenGroupsComponent {

@ViewChild('defaultCard') defaultCardTemplate: TemplateRef<any>;
@ViewChild('assigneeCard') assigneeCardTemplate: TemplateRef<any>;
@ViewChild('milestoneCard') milestoneCardTemplate: TemplateRef<any>;

constructor(public groupingContextService: GroupingContextService) {}

getCardTemplate(): TemplateRef<any> {
switch (this.groupingContextService.currGroupBy) {
case GroupBy.Assignee:
return this.assigneeCardTemplate;
case GroupBy.Milestone:
return this.milestoneCardTemplate;
default:
return this.defaultCardTemplate;
}
Expand Down
Loading

0 comments on commit f53a718

Please sign in to comment.