diff --git a/src/app/activity-dashboard/event-tables/GithubEventDataTable.ts b/src/app/activity-dashboard/event-tables/GithubEventDataTable.ts index b7f1a7aa3..663849e4e 100644 --- a/src/app/activity-dashboard/event-tables/GithubEventDataTable.ts +++ b/src/app/activity-dashboard/event-tables/GithubEventDataTable.ts @@ -12,7 +12,7 @@ import { EventWeek } from '../event-week.model'; import { paginateData } from './event-paginator'; /** - * Adapted from IssuesDataTable for Events. + * Adapted from RepoItemsDataTable for Events. */ export class GithubEventDataTable extends DataSource { private startDate = new BehaviorSubject(''); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7cdebaef2..fff741ccd 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,12 +4,12 @@ import { ActivityDashboardModule } from './activity-dashboard/activity-dashboard import { AuthModule } from './auth/auth.module'; import { AuthGuard } from './core/guards/auth.guard'; import { ParseUrlParamsGuard } from './core/guards/parse-url-params.guard'; -import { IssuesViewerModule } from './issues-viewer/issues-viewer.module'; +import { RepoItemsViewerModule } from './repo-items-viewer/repo-items-viewer.module'; const routes: Routes = [ { path: '', loadChildren: () => AuthModule }, - { path: 'issuesViewer/:org/:repo', canActivate: [ParseUrlParamsGuard], children: [] }, - { path: 'issuesViewer', loadChildren: () => IssuesViewerModule, canLoad: [AuthGuard] }, + { path: 'repoItemsViewer/:org/:repo', canActivate: [ParseUrlParamsGuard], children: [] }, + { path: 'repoItemsViewer', loadChildren: () => RepoItemsViewerModule, canLoad: [AuthGuard] }, { path: 'activityDashboard', loadChildren: () => ActivityDashboardModule, canLoad: [AuthGuard] } ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index be572020c..e70155be6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -21,16 +21,16 @@ import { ErrorHandlingService } from './core/services/error-handling.service'; import { ErrorMessageService } from './core/services/error-message.service'; import { AuthServiceFactory } from './core/services/factories/factory.auth.service'; import { GithubServiceFactory } from './core/services/factories/factory.github.service'; -import { IssueServiceFactory } from './core/services/factories/factory.issue.service'; +import { RepoItemServiceFactory } from './core/services/factories/factory.issue.service'; import { GithubService } from './core/services/github.service'; import { GithubEventService } from './core/services/githubevent.service'; -import { IssueService } from './core/services/issue.service'; import { LabelService } from './core/services/label.service'; import { LoggingService } from './core/services/logging.service'; +import { RepoItemService } from './core/services/repo-item.service'; import { RepoSessionStorageService } from './core/services/repo-session-storage.service'; import { UserService } from './core/services/user.service'; import { ViewService } from './core/services/view.service'; -import { IssuesViewerModule } from './issues-viewer/issues-viewer.module'; +import { RepoItemsViewerModule } from './repo-items-viewer/repo-items-viewer.module'; import { LabelDefinitionPopupComponent } from './shared/label-definition-popup/label-definition-popup.component'; import { HeaderComponent } from './shared/layout'; import { RepoChangeFormComponent } from './shared/repo-change-form/repo-change-form.component'; @@ -42,7 +42,7 @@ import { SharedModule } from './shared/shared.module'; BrowserModule, BrowserAnimationsModule, AuthModule, - IssuesViewerModule, + RepoItemsViewerModule, ActivityDashboardModule, SharedModule, HttpClientModule, @@ -62,7 +62,7 @@ import { SharedModule } from './shared/shared.module'; NgZone, GithubService, UserService, - IssueService, + RepoItemService, LabelService, ViewService, GithubEventService, @@ -72,8 +72,8 @@ import { SharedModule } from './shared/shared.module'; ] }, { - provide: IssueService, - useFactory: IssueServiceFactory, + provide: RepoItemService, + useFactory: RepoItemServiceFactory, deps: [GithubService, UserService, ViewService] }, { diff --git a/src/app/core/guards/parse-url-params.guard.ts b/src/app/core/guards/parse-url-params.guard.ts index c73129a53..442112779 100644 --- a/src/app/core/guards/parse-url-params.guard.ts +++ b/src/app/core/guards/parse-url-params.guard.ts @@ -14,7 +14,7 @@ export class ParseUrlParamsGuard implements CanActivate { /** * Saves org/repo url parameters to session storage * Redirects to '/' as login session not persistant - * This keeps /issuesViewer route protected + * This keeps /repoItemsViewer route protected * @return false */ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { diff --git a/src/app/core/models/github-user.model.ts b/src/app/core/models/github-user.model.ts index 50e59c1bd..c9750dfae 100644 --- a/src/app/core/models/github-user.model.ts +++ b/src/app/core/models/github-user.model.ts @@ -55,7 +55,7 @@ export class GithubUser implements RawGithubUser, Group { /* This method is used to enable comparisons between a Group and the filtering criteria, which is stored - as a string, in IssuesDataTable.ts + as a string, in RepoItemsDataTable.ts */ static fromUsername(username: string) { return new GithubUser({ diff --git a/src/app/core/models/github/github-issue.model.ts b/src/app/core/models/github/github-issue.model.ts index 64ff869a5..0b5b9f805 100644 --- a/src/app/core/models/github/github-issue.model.ts +++ b/src/app/core/models/github/github-issue.model.ts @@ -1,6 +1,6 @@ import { IssueState, IssueStateReason } from '../../../../../graphql/graphql-types'; -import { ReviewDecision } from '../issue.model'; import { PullrequestReviewState } from '../pullrequest-review.model'; +import { ReviewDecision } from '../repo-item.model'; import { GithubComment } from './github-comment.model'; import { GithubLabel } from './github-label.model'; diff --git a/src/app/core/models/issue.model.ts b/src/app/core/models/issue.model.ts index ce9c0dbe4..4562fb9f8 100644 --- a/src/app/core/models/issue.model.ts +++ b/src/app/core/models/issue.model.ts @@ -1,118 +1,14 @@ -import * as moment from 'moment'; -import { GithubComment } from './github/github-comment.model'; import { GithubIssue } from './github/github-issue.model'; -import { GithubLabel } from './github/github-label.model'; -import { HiddenData } from './hidden-data.model'; import { Milestone } from './milestone.model'; -import { PullrequestReview } from './pullrequest-review.model'; - -export enum ReviewDecision { - CHANGES_REQUESTED = 'CHANGES_REQUESTED', - APPROVED = 'APPROVED', - REVIEW_REQUIRED = 'REVIEW_REQUIRED' -} - -export class Issue { - /** Basic Fields */ - readonly globalId: string; - readonly id: number; - readonly created_at: string; - readonly githubIssue: GithubIssue; - githubComments: GithubComment[]; - title: string; - description: string; - hiddenDataInDescription: HiddenData; - updated_at: string; - closed_at: string; - milestone: Milestone; - state: string; - stateReason: string; - issueOrPr: string; - author: string; - headRepository: string; - isDraft: boolean; - - /** Depending on the view, assignees attribute can be derived from Github's assignee feature OR from the Github's issue description */ - assignees?: string[]; - labels?: string[]; - githubLabels?: GithubLabel[]; - reviews?: PullrequestReview[]; - reviewDecision?: ReviewDecision | null; - - /** - * Formats the text to create space at the end of the user input to prevent any issues with - * the markdown interpretation. - * - * Brought over from comment-editor.component.ts - */ - static formatText(text: string): string { - if (text === null) { - return null; - } - - if (text === undefined) { - return undefined; - } - - const newLinesRegex = /[\n\r]/gi; - const textSplitArray = text.split(newLinesRegex); - if (textSplitArray.filter((split) => split.trim() !== '').length > 0) { - return `${text}\n\n`; - } else { - return text; - } - } - - /** - * Processes and cleans a raw issue description obtained from user input. - */ - static updateDescription(description: string): string { - const defaultString = 'No details provided by bug reporter.'; - return Issue.orDefaultString(Issue.formatText(description), defaultString); - } - - /** - * Given two strings, returns the first if it is not an empty string or a false value such as null/undefined. - * Returns the second string if the first is an empty string. - */ - private static orDefaultString(stringA: string, def: string): string { - if (!stringA) { - return def; - } - return stringA.length !== 0 ? stringA : def; - } +import { RepoItem } from './repo-item.model'; +export class Issue extends RepoItem { protected constructor(githubIssue: GithubIssue) { - /** Basic Fields */ - this.globalId = githubIssue.id; - this.id = +githubIssue.number; - this.created_at = moment(githubIssue.created_at).format('lll'); - this.updated_at = moment(githubIssue.updated_at).format('lll'); - this.closed_at = moment(githubIssue.closed_at).format('lll'); - this.title = githubIssue.title; - this.hiddenDataInDescription = new HiddenData(githubIssue.body); - this.description = Issue.updateDescription(this.hiddenDataInDescription.originalStringWithoutHiddenData); - this.state = githubIssue.state; - this.stateReason = githubIssue.stateReason; - this.issueOrPr = githubIssue.issueOrPr; - this.author = githubIssue.user.login; - // this.githubIssue = githubIssue; - this.isDraft = githubIssue.isDraft; - this.reviewDecision = githubIssue.reviewDecision; - - 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; - this.headRepository = githubIssue.headRepository?.nameWithOwner; - this.reviews = githubIssue.reviews?.map((review) => new PullrequestReview(review)); + super(githubIssue, 'Issue'); + this.milestone = githubIssue.milestone ? new Milestone(githubIssue.milestone) : Milestone.IssueWithoutMilestone; } - public static createPhaseBugReportingIssue(githubIssue: GithubIssue): Issue { + public static createIssue(githubIssue: GithubIssue): Issue { return new Issue(githubIssue); } @@ -124,11 +20,3 @@ export class Issue { export interface Issues { [id: number]: Issue; } - -export const IssuesFilter = { - issuesViewer: { - Student: 'NO_FILTER', - Tutor: 'NO_FILTER', - Admin: 'NO_FILTER' - } -}; diff --git a/src/app/core/models/milestone.model.ts b/src/app/core/models/milestone.model.ts index 88a06d59d..99c027521 100644 --- a/src/app/core/models/milestone.model.ts +++ b/src/app/core/models/milestone.model.ts @@ -21,7 +21,7 @@ export class Milestone implements Group { /* This method is used to enable comparisons between a Group and the filtering criteria, which is stored - as a string, in IssuesDataTable.ts + as a string, in RepoItemsDataTable.ts */ static fromTitle(title: string): Milestone { return new Milestone({ title, state: '' }); diff --git a/src/app/core/models/pull-request.model.ts b/src/app/core/models/pull-request.model.ts new file mode 100644 index 000000000..a8d705d60 --- /dev/null +++ b/src/app/core/models/pull-request.model.ts @@ -0,0 +1,18 @@ +import { GithubIssue } from './github/github-issue.model'; +import { Milestone } from './milestone.model'; +import { RepoItem } from './repo-item.model'; + +export class PullRequest extends RepoItem { + protected constructor(githubIssue: GithubIssue) { + super(githubIssue, 'PullRequest'); + this.milestone = githubIssue.milestone ? new Milestone(githubIssue.milestone) : Milestone.PRWithoutMilestone; + } + + public static createPullRequest(githubIssue: GithubIssue): RepoItem { + return new PullRequest(githubIssue); + } +} + +export interface PullRequests { + [id: number]: PullRequests; +} diff --git a/src/app/core/models/repo-item.model.ts b/src/app/core/models/repo-item.model.ts new file mode 100644 index 000000000..f9f562171 --- /dev/null +++ b/src/app/core/models/repo-item.model.ts @@ -0,0 +1,125 @@ +import * as moment from 'moment'; +import { GithubComment } from './github/github-comment.model'; +import { GithubIssue } from './github/github-issue.model'; +import { GithubLabel } from './github/github-label.model'; +import { HiddenData } from './hidden-data.model'; +import { Milestone } from './milestone.model'; +import { PullrequestReview } from './pullrequest-review.model'; + +export enum ReviewDecision { + CHANGES_REQUESTED = 'CHANGES_REQUESTED', + APPROVED = 'APPROVED', + REVIEW_REQUIRED = 'REVIEW_REQUIRED' +} + +export abstract class RepoItem { + /** Basic Fields */ + readonly type: string; + readonly globalId: string; + readonly id: number; + readonly created_at: string; + readonly githubIssue: GithubIssue; + githubComments: GithubComment[]; + title: string; + description: string; + hiddenDataInDescription: HiddenData; + updated_at: string; + closed_at: string; + milestone: Milestone; + state: string; + stateReason: string; + author: string; + headRepository: string; + isDraft: boolean; + + /** Depending on the view, assignees attribute can be derived from Github's assignee feature OR from the Github's issue description */ + assignees?: string[]; + labels?: string[]; + githubLabels?: GithubLabel[]; + reviews?: PullrequestReview[]; + reviewDecision?: ReviewDecision | null; + + /** + * Formats the text to create space at the end of the user input to prevent any issues with + * the markdown interpretation. + * + * Brought over from comment-editor.component.ts + */ + static formatText(text: string): string { + if (text === null) { + return null; + } + + if (text === undefined) { + return undefined; + } + + const newLinesRegex = /[\n\r]/gi; + const textSplitArray = text.split(newLinesRegex); + if (textSplitArray.filter((split) => split.trim() !== '').length > 0) { + return `${text}\n\n`; + } else { + return text; + } + } + + /** + * Processes and cleans a raw issue description obtained from user input. + */ + static updateDescription(description: string): string { + const defaultString = 'No details provided by bug reporter.'; + return RepoItem.orDefaultString(RepoItem.formatText(description), defaultString); + } + + /** + * Given two strings, returns the first if it is not an empty string or a false value such as null/undefined. + * Returns the second string if the first is an empty string. + */ + private static orDefaultString(stringA: string, def: string): string { + if (!stringA) { + return def; + } + return stringA.length !== 0 ? stringA : def; + } + + protected constructor(githubIssue: GithubIssue, type: string = 'RepoItem') { + /** Basic Fields */ + this.type = type; + this.globalId = githubIssue.id; + this.id = +githubIssue.number; + this.created_at = moment(githubIssue.created_at).format('lll'); + this.updated_at = moment(githubIssue.updated_at).format('lll'); + this.closed_at = moment(githubIssue.closed_at).format('lll'); + this.title = githubIssue.title; + this.hiddenDataInDescription = new HiddenData(githubIssue.body); + this.description = RepoItem.updateDescription(this.hiddenDataInDescription.originalStringWithoutHiddenData); + this.state = githubIssue.state; + this.stateReason = githubIssue.stateReason; + this.author = githubIssue.user.login; + // this.githubIssue = githubIssue; + this.isDraft = githubIssue.isDraft; + this.reviewDecision = githubIssue.reviewDecision; + + this.assignees = githubIssue.assignees.map((assignee) => assignee.login); + this.githubLabels = githubIssue.labels; + this.labels = githubIssue.labels.map((label) => label.name); + this.headRepository = githubIssue.headRepository?.nameWithOwner; + this.reviews = githubIssue.reviews?.map((review) => new PullrequestReview(review)); + } + + createGithubIssueDescription(): string { + return `${this.description}\n${this.hiddenDataInDescription.toString()}`; + } +} + +export interface RepoItems { + [id: number]: RepoItem; +} + +export const RepoItemFilter = { + repoItemsViewer: { + Student: 'NO_FILTER', + Tutor: 'NO_FILTER', + Admin: 'NO_FILTER' + } +}; diff --git a/src/app/core/models/view.model.ts b/src/app/core/models/view.model.ts index 5fdc2809e..d3ac405b6 100644 --- a/src/app/core/models/view.model.ts +++ b/src/app/core/models/view.model.ts @@ -1,4 +1,4 @@ export enum View { - issuesViewer = 'issuesViewer', + repoItemsViewer = 'repoItemsViewer', activityDashboard = 'activityDashboard' } diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 02bebbb24..221f6135c 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -10,9 +10,9 @@ import { View } from '../models/view.model'; import { ErrorHandlingService } from './error-handling.service'; import { GithubService } from './github.service'; import { GithubEventService } from './githubevent.service'; -import { IssueService } from './issue.service'; import { LabelService } from './label.service'; import { LoggingService } from './logging.service'; +import { RepoItemService } from './repo-item.service'; import { UserService } from './user.service'; import { ViewService } from './view.service'; @@ -47,7 +47,7 @@ export class AuthService { private ngZone: NgZone, private githubService: GithubService, private userService: UserService, - private issueService: IssueService, + private repoItemService: RepoItemService, private labelService: LabelService, private viewService: ViewService, private githubEventService: GithubEventService, @@ -153,13 +153,13 @@ export class AuthService { logOut(): void { this.githubService.reset(); this.userService.reset(); - this.issueService.reset(true); + this.repoItemService.reset(true); this.labelService.reset(); this.viewService.reset(); this.githubEventService.reset(); this.logger.reset(); this.setLandingPageTitle(); - this.issueService.setIssueTeamFilter('All Teams'); + this.repoItemService.setRepoItemTeamFilter('All Teams'); this.reset(); } @@ -184,7 +184,7 @@ export class AuthService { changeAuthState(newAuthState: AuthState) { if (newAuthState === AuthState.Authenticated) { const sessionId = generateSessionId(); - this.issueService.setSessionId(sessionId); + this.repoItemService.setSessionId(sessionId); this.logger.info(`AuthService: Successfully authenticated with session: ${sessionId}`); } this.authStateSource.next(newAuthState); @@ -234,7 +234,7 @@ export class AuthService { */ handleSetRepoSuccess(repoName: string) { this.setTitleWithViewDetail(); - this.router.navigate([View.issuesViewer], { + this.router.navigate([View.repoItemsViewer], { queryParams: { [ViewService.REPO_QUERY_PARAM_KEY]: repoName } diff --git a/src/app/core/services/factories/factory.auth.service.ts b/src/app/core/services/factories/factory.auth.service.ts index 2aa915b76..a868f0a5f 100644 --- a/src/app/core/services/factories/factory.auth.service.ts +++ b/src/app/core/services/factories/factory.auth.service.ts @@ -6,9 +6,9 @@ import { AuthService } from '../auth.service'; import { ErrorHandlingService } from '../error-handling.service'; import { GithubService } from '../github.service'; import { GithubEventService } from '../githubevent.service'; -import { IssueService } from '../issue.service'; import { LabelService } from '../label.service'; import { LoggingService } from '../logging.service'; +import { RepoItemService } from '../repo-item.service'; // import { MockAuthService } from '../mocks/mock.auth.service'; import { UserService } from '../user.service'; import { ViewService } from '../view.service'; @@ -18,7 +18,7 @@ export function AuthServiceFactory( ngZone: NgZone, githubService: GithubService, userService: UserService, - issueService: IssueService, + repoItemService: RepoItemService, labelService: LabelService, viewService: ViewService, githubEventService: GithubEventService, @@ -33,7 +33,7 @@ export function AuthServiceFactory( // ngZone, // githubService, // userService, - // issueService, + // repoItemService, // labelService, // viewService, // githubEventService, @@ -46,7 +46,7 @@ export function AuthServiceFactory( ngZone, githubService, userService, - issueService, + repoItemService, labelService, viewService, githubEventService, diff --git a/src/app/core/services/factories/factory.issue.service.ts b/src/app/core/services/factories/factory.issue.service.ts index 719bc9038..8c6dd52ff 100644 --- a/src/app/core/services/factories/factory.issue.service.ts +++ b/src/app/core/services/factories/factory.issue.service.ts @@ -1,14 +1,14 @@ // import { AppConfig } from '../../../../environments/environment'; import { GithubService } from '../github.service'; -import { IssueService } from '../issue.service'; +import { RepoItemService } from '../repo-item.service'; // import { MockIssueService } from '../mocks/mock.issue.service'; import { UserService } from '../user.service'; import { ViewService } from '../view.service'; -export function IssueServiceFactory(githubService: GithubService, userService: UserService, viewService: ViewService) { +export function RepoItemServiceFactory(githubService: GithubService, userService: UserService, viewService: ViewService) { // TODO: Write Mocks // if (AppConfig.test) { // return new MockIssueService(githubService, viewService, dataService); // } - return new IssueService(githubService, userService, viewService); + return new RepoItemService(githubService, userService, viewService); } diff --git a/src/app/core/services/githubevent.service.ts b/src/app/core/services/githubevent.service.ts index 070e6714c..580324670 100644 --- a/src/app/core/services/githubevent.service.ts +++ b/src/app/core/services/githubevent.service.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, EMPTY, Observable, of, Subscription, timer } from 'rxj import { catchError, exhaustMap, finalize, flatMap, map } from 'rxjs/operators'; import { GithubEvent } from '../models/github/github-event.model'; import { GithubService } from './github.service'; -import { IssueService } from './issue.service'; +import { RepoItemService } from './repo-item.service'; @Injectable({ providedIn: 'root' @@ -20,7 +20,7 @@ export class GithubEventService { private lastModified: string; // The timestamp when the title or label of an issue is changed private lastModifiedComment: string; // The timestamp when the comment of an issue is changed - constructor(private githubService: GithubService, private issueService: IssueService) { + constructor(private githubService: GithubService, private repoItemService: RepoItemService) { this.events$ = new BehaviorSubject(new Array()); } @@ -58,7 +58,7 @@ export class GithubEventService { if (eventResponse['created_at'] !== this.lastModified || eventResponse['issue']['updated_at'] !== this.lastModifiedComment) { this.setLastModifiedTime(eventResponse['created_at']); this.setLastModifiedCommentTime(eventResponse['issue']['updated_at']); - return this.issueService.reloadAllIssues().pipe(map((response: any[]) => true)); + return this.repoItemService.reloadAllRepoItems().pipe(map((response: any[]) => true)); } return of(false); }) @@ -97,7 +97,7 @@ export class GithubEventService { } } - this.eventsPollSubscription = timer(0, IssueService.POLL_INTERVAL) + this.eventsPollSubscription = timer(0, RepoItemService.POLL_INTERVAL) .pipe( exhaustMap(() => { return this.getEvents().pipe( diff --git a/src/app/core/services/grouping/assignee-grouping-strategy.service.ts b/src/app/core/services/grouping/assignee-grouping-strategy.service.ts index 225f07e36..635c2f5c0 100644 --- a/src/app/core/services/grouping/assignee-grouping-strategy.service.ts +++ b/src/app/core/services/grouping/assignee-grouping-strategy.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { GithubUser } from '../../models/github-user.model'; -import { Issue } from '../../models/issue.model'; +import { PullRequest } from '../../models/pull-request.model'; +import { RepoItem } from '../../models/repo-item.model'; import { GithubService } from '../github.service'; import { GroupingStrategy } from './grouping-strategy.interface'; @@ -20,11 +21,11 @@ export class AssigneeGroupingStrategy implements GroupingStrategy { * If it is the "No Assignee" group, unassigned issues are returned. * Otherwise, issues assigned to the specified user are returned. */ - getDataForGroup(issues: Issue[], key: GithubUser): Issue[] { + getDataForGroup(items: RepoItem[], key: GithubUser): RepoItem[] { if (key === GithubUser.NO_ASSIGNEE) { - return this.getUnassignedData(issues); + return this.getUnassignedData(items); } - return this.getDataAssignedToUser(issues, key); + return this.getDataAssignedToUser(items, key); } /** @@ -48,33 +49,33 @@ export class AssigneeGroupingStrategy implements GroupingStrategy { return group !== GithubUser.NO_ASSIGNEE; } - private getDataAssignedToUser(issues: Issue[], user: GithubUser): Issue[] { - const filteredIssues = issues.filter((issue) => { - if (this.isPullRequest(issue)) { - return this.isPullRequestCreatedByTarget(issue, user); + private getDataAssignedToUser(items: RepoItem[], user: GithubUser): RepoItem[] { + const filteredIssues = items.filter((item) => { + if (this.isPullRequest(item)) { + return this.isRepoItemCreatedByTarget(item, user); } - return this.isIssueAssignedToTarget(issue, user); + return this.isRepoItemAssignedToTarget(item, user); }); return filteredIssues; } - private getUnassignedData(issues: Issue[]): Issue[] { - return issues.filter((issue) => !this.isPullRequest(issue) && issue.assignees.length === 0); + private getUnassignedData(items: RepoItem[]): RepoItem[] { + return items.filter((item) => !this.isPullRequest(item) && item.assignees.length === 0); } - private isPullRequest(issue: Issue): boolean { - return issue.issueOrPr === 'PullRequest'; + private isPullRequest(item: RepoItem): boolean { + return item.type === 'PullRequest'; } - private isPullRequestCreatedByTarget(issue: Issue, target: GithubUser): boolean { - return issue.author === target.login; + private isRepoItemCreatedByTarget(item: RepoItem, target: GithubUser): boolean { + return item.author === target.login; } - private isIssueAssignedToTarget(issue: Issue, target: GithubUser): boolean { - const isAssigneesFieldDefined = !!issue.assignees; + private isRepoItemAssignedToTarget(item: RepoItem, target: GithubUser): boolean { + const isAssigneesFieldDefined = !!item.assignees; - return isAssigneesFieldDefined && issue.assignees.includes(target.login); + return isAssigneesFieldDefined && item.assignees.includes(target.login); } } diff --git a/src/app/core/services/grouping/grouping-context.service.ts b/src/app/core/services/grouping/grouping-context.service.ts index 0a3319e5b..26382ba61 100644 --- a/src/app/core/services/grouping/grouping-context.service.ts +++ b/src/app/core/services/grouping/grouping-context.service.ts @@ -2,7 +2,7 @@ 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 { RepoItem } from '../../models/repo-item.model'; import { AssigneeGroupingStrategy } from './assignee-grouping-strategy.service'; import { GroupingStrategy } from './grouping-strategy.interface'; import { MilestoneGroupingStrategy } from './milestone-grouping-strategy.service'; @@ -75,13 +75,13 @@ export class GroupingContextService { /** * Retrieves data for a specific group. - * @param issues - An array of issues to be grouped. - * @param group - The group by which issues are to be grouped. - * @returns An array of issues belonging to the specified group. + * @param items - An array of items to be grouped. + * @param group - The group by which items are to be grouped. + * @returns An array of items belonging to the specified group. */ - getDataForGroup(issues: Issue[], group: Group): Issue[] { + getDataForGroup(items: RepoItem[], group: Group): RepoItem[] { const strategy = this.groupingStrategyMap.get(this.currGroupBy); - return strategy.getDataForGroup(issues, group); + return strategy.getDataForGroup(items, group); } /** diff --git a/src/app/core/services/grouping/grouping-strategy.interface.ts b/src/app/core/services/grouping/grouping-strategy.interface.ts index 6c2f407ee..dd222bb20 100644 --- a/src/app/core/services/grouping/grouping-strategy.interface.ts +++ b/src/app/core/services/grouping/grouping-strategy.interface.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs'; import { Group } from '../../models/github/group.interface'; -import { Issue } from '../../models/issue.model'; +import { RepoItem } from '../../models/repo-item.model'; /** * Represent a strategy for grouping issues/prs. @@ -14,7 +14,7 @@ export interface GroupingStrategy { * @param key - The group by which issues are to be grouped. * @returns An array of issues belonging to the specified group. */ - getDataForGroup(issues: Issue[], key: Group): Issue[]; + getDataForGroup(items: RepoItem[], key: Group): RepoItem[]; /** * Retrieves observable emitting groups available for the grouping strategy. diff --git a/src/app/core/services/grouping/milestone-grouping-strategy.service.ts b/src/app/core/services/grouping/milestone-grouping-strategy.service.ts index ff501fb48..e720e82d8 100644 --- a/src/app/core/services/grouping/milestone-grouping-strategy.service.ts +++ b/src/app/core/services/grouping/milestone-grouping-strategy.service.ts @@ -1,8 +1,8 @@ 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 { RepoItem } from '../../models/repo-item.model'; import { MilestoneService } from '../milestone.service'; import { GroupingStrategy } from './grouping-strategy.interface'; @@ -18,8 +18,8 @@ export class MilestoneGroupingStrategy implements GroupingStrategy { /** * Retrieves data for a milestone. */ - getDataForGroup(issues: Issue[], key: Milestone): Issue[] { - return issues.filter((issue) => issue.milestone.equals(key)); + getDataForGroup(items: RepoItem[], key: Milestone): RepoItem[] { + return items.filter((item) => item.milestone.equals(key)); } /** diff --git a/src/app/core/services/issue.service.ts b/src/app/core/services/issue.service.ts deleted file mode 100644 index 2c66051b9..000000000 --- a/src/app/core/services/issue.service.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, of, Subscription, throwError, timer } from 'rxjs'; -import { catchError, exhaustMap, finalize, map } from 'rxjs/operators'; -import RestGithubIssueFilter from '../models/github/github-issue-filter.model'; -import { GithubIssue } from '../models/github/github-issue.model'; -import { Issue, Issues, IssuesFilter } from '../models/issue.model'; -import { View } from '../models/view.model'; -import { GithubService } from './github.service'; -import { UserService } from './user.service'; -import { ViewService } from './view.service'; - -@Injectable({ - providedIn: 'root' -}) - -/** - * Responsible for creating and updating issues, and periodically fetching issues - * using GitHub. - */ -export class IssueService { - static readonly POLL_INTERVAL = 20000; // 20 seconds - - issues: Issues; - issues$: BehaviorSubject; - - private sessionId: string; - private issueTeamFilter = 'All Teams'; - private issuesPollSubscription: Subscription; - /** Whether the IssueService is downloading the data from Github*/ - public isLoading = new BehaviorSubject(false); - - constructor(private githubService: GithubService, private userService: UserService, private viewService: ViewService) { - this.issues$ = new BehaviorSubject(new Array()); - } - - startPollIssues() { - if (this.issuesPollSubscription === undefined) { - if (this.issues$.getValue().length === 0) { - this.isLoading.next(true); - } - - this.issuesPollSubscription = timer(0, IssueService.POLL_INTERVAL) - .pipe( - exhaustMap(() => { - return this.reloadAllIssues().pipe( - catchError((err) => throwError(err)), - finalize(() => this.isLoading.next(false)) - ); - }) - ) - .subscribe(); - } - } - - stopPollIssues() { - if (this.issuesPollSubscription) { - this.issuesPollSubscription.unsubscribe(); - this.issuesPollSubscription = undefined; - } - } - - reloadAllIssues() { - return this.initializeData(); - } - - getIssue(id: number): Observable { - if (this.issues === undefined) { - return this.getLatestIssue(id); - } else { - return of(this.issues[id]); - } - } - - getLatestIssue(id: number): Observable { - return this.githubService.fetchIssueGraphql(id).pipe( - map((response: GithubIssue) => { - this.createAndSaveIssueModels([response]); - return this.issues[id]; - }), - catchError((err) => { - return of(this.issues[id]); - }) - ); - } - - /** - * This function will update the issue's state of the application. This function needs to be called whenever a issue is added/updated. - * - * @params issuesToUpdate - An array of issues to update the state of the application with. - */ - private updateLocalStore(issuesToUpdate: Issue[]) { - const newIssues = { ...this.issues }; - issuesToUpdate.forEach((issue) => { - newIssues[issue.id] = issue; - }); - this.issues = newIssues; - - this.issues$.next(Object.values(this.issues)); - } - - reset(resetSessionId: boolean) { - if (resetSessionId) { - this.sessionId = undefined; - } - - this.issues = undefined; - this.issues$.next(new Array()); - - this.stopPollIssues(); - } - - private initializeData(): Observable { - let issuesAPICallsByFilter: Observable>; - - switch (IssuesFilter[this.viewService.currentView][this.userService.currentUser.role]) { - case 'FILTER_BY_CREATOR': - issuesAPICallsByFilter = this.githubService.fetchIssuesGraphql( - new RestGithubIssueFilter({ creator: this.userService.currentUser.loginId }) - ); - break; - case 'NO_FILTER': - issuesAPICallsByFilter = this.githubService.fetchIssuesGraphql(new RestGithubIssueFilter({})); - break; - case 'NO_ACCESS': - default: - return of([]); - } - - const fetchedIssueIds: number[] = []; - - return issuesAPICallsByFilter.pipe( - map((githubIssues: GithubIssue[]) => { - const issues = this.createAndSaveIssueModels(githubIssues); - for (const issue of issues) { - fetchedIssueIds.push(issue.id); - } - - const outdatedIssueIds: number[] = this.getOutdatedIssueIds(fetchedIssueIds); - this.deleteIssuesFromLocalStore(outdatedIssueIds); - - if (this.issues === undefined) { - return []; - } - return Object.values(this.issues); - }) - ); - } - - private createAndSaveIssueModels(githubIssues: GithubIssue[]): Issue[] { - const issues: Issue[] = []; - - for (const githubIssue of githubIssues) { - const issue = this.createIssueModel(githubIssue); - issues.push(issue); - } - this.updateLocalStore(issues); - - return issues; - } - - private deleteIssuesFromLocalStore(ids: number[]): void { - const withoutIssuesToRemove = { ...this.issues }; - for (const id of ids) { - delete withoutIssuesToRemove[id]; - } - - this.issues = withoutIssuesToRemove; - - this.issues$.next(Object.values(this.issues)); - } - - /** - * Returns an array of outdated issue ids by comparing the ids of the recently - * fetched issues with the current issue ids in the local store - */ - private getOutdatedIssueIds(fetchedIssueIds: number[]): number[] { - /* - Ignore for first fetch or ignore if there is no fetch result - - We also have to ignore for no fetch result as the cache might return a - 304 reponse with no differences in issues, resulting in the fetchIssueIds - to be empty - */ - if (this.issues === undefined || !fetchedIssueIds.length) { - return []; - } - - const fetchedIssueIdsSet = new Set(fetchedIssueIds); - - const result = Object.keys(this.issues) - .map((x) => +x) - .filter((issueId) => !fetchedIssueIdsSet.has(issueId)); - - return result; - } - - private createIssueModel(githubIssue: GithubIssue): Issue { - switch (this.viewService.currentView) { - case View.issuesViewer: - return Issue.createPhaseBugReportingIssue(githubIssue); - default: - return; - } - } - - setIssueTeamFilter(filterValue: string) { - if (filterValue) { - this.issueTeamFilter = filterValue; - } - } - - setSessionId(sessionId: string) { - this.sessionId = sessionId; - } - - getIssueTeamFilter(): string { - return this.issueTeamFilter; - } -} diff --git a/src/app/core/services/repo-item.service.ts b/src/app/core/services/repo-item.service.ts new file mode 100644 index 000000000..fab006fa4 --- /dev/null +++ b/src/app/core/services/repo-item.service.ts @@ -0,0 +1,235 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, of, Subscription, throwError, timer } from 'rxjs'; +import { catchError, exhaustMap, finalize, map } from 'rxjs/operators'; +import RestGithubRepoItemFilter from '../models/github/github-issue-filter.model'; +import { GithubIssue } from '../models/github/github-issue.model'; +import { Issue } from '../models/issue.model'; +import { PullRequest } from '../models/pull-request.model'; +import { RepoItem, RepoItemFilter, RepoItems } from '../models/repo-item.model'; +import { View } from '../models/view.model'; +import { GithubService } from './github.service'; +import { UserService } from './user.service'; +import { ViewService } from './view.service'; + +@Injectable({ + providedIn: 'root' +}) + +/** + * Responsible for creating and updating repo items, and periodically fetching issues + * using GitHub. + */ +export class RepoItemService { + static readonly POLL_INTERVAL = 20000; // 20 seconds + + repoItem: RepoItems; + repoItem$: BehaviorSubject; + + private sessionId: string; + private repoItemTeamFilter = 'All Teams'; + private repoItemPollSubscription: Subscription; + /** Whether the RepoItemService is downloading the repoItem from Github*/ + public isLoading = new BehaviorSubject(false); + + constructor(private githubService: GithubService, private userService: UserService, private viewService: ViewService) { + this.repoItem$ = new BehaviorSubject(new Array()); + } + + startPollRepoItems() { + if (this.repoItemPollSubscription === undefined) { + if (this.repoItem$.getValue().length === 0) { + this.isLoading.next(true); + } + + this.repoItemPollSubscription = timer(0, RepoItemService.POLL_INTERVAL) + .pipe( + exhaustMap(() => { + return this.reloadAllRepoItems().pipe( + catchError((err) => throwError(err)), + finalize(() => this.isLoading.next(false)) + ); + }) + ) + .subscribe(); + } + } + + stopPollRepoItems() { + if (this.repoItemPollSubscription) { + this.repoItemPollSubscription.unsubscribe(); + this.repoItemPollSubscription = undefined; + } + } + + reloadAllRepoItems() { + return this.initializeData(); + } + + getRepoItem(id: number): Observable { + if (this.repoItem === undefined) { + return this.getLatestRepoItem(id); + } else { + return of(this.repoItem[id]); + } + } + + getLatestRepoItem(id: number): Observable { + return this.githubService.fetchIssueGraphql(id).pipe( + map((response: GithubIssue) => { + this.createAndSaveRepoItemModels([response]); + return this.repoItem[id]; + }), + catchError((err) => { + return of(this.repoItem[id]); + }) + ); + } + + /** + * This function will update the repo item's state of the application. + * This function needs to be called whenever a repo item is added/updated. + * + * @params dataToUpdate - An array of data (repo items) to update the state of the application with. + */ + private updateLocalStore(dataToUpdate: RepoItem[]) { + const newData = { ...this.repoItem }; + dataToUpdate.forEach((datum) => { + newData[datum.id] = datum; + }); + this.repoItem = newData; + + this.repoItem$.next(Object.values(this.repoItem)); + } + + reset(resetSessionId: boolean) { + if (resetSessionId) { + this.sessionId = undefined; + } + + this.repoItem = undefined; + this.repoItem$.next(new Array()); + + this.stopPollRepoItems(); + } + + private initializeData(): Observable { + let issuesAPICallsByFilter: Observable>; + + const filter = RepoItemFilter[this.viewService.currentView][this.userService.currentUser.role]; + switch (filter) { + case 'FILTER_BY_CREATOR': + issuesAPICallsByFilter = this.githubService.fetchIssuesGraphql( + new RestGithubRepoItemFilter({ creator: this.userService.currentUser.loginId }) + ); + break; + case 'NO_FILTER': + issuesAPICallsByFilter = this.githubService.fetchIssuesGraphql(new RestGithubRepoItemFilter({})); + break; + case 'NO_ACCESS': + default: + return of([]); + } + + const fetchedRepoItemIds: number[] = []; + + return issuesAPICallsByFilter.pipe( + map((githubRepoItems: GithubIssue[]) => { + const repoItems = this.createAndSaveRepoItemModels(githubRepoItems); + for (const repoItem of repoItems) { + fetchedRepoItemIds.push(repoItem.id); + } + + const outdatedRepoItemIds: number[] = this.getOutdatedRepoItemIds(fetchedRepoItemIds); + this.deleteRepoItemsFromLocalStore(outdatedRepoItemIds); + + if (this.repoItem === undefined) { + return []; + } + return Object.values(this.repoItem); + }) + ); + } + + private createAndSaveRepoItemModels(githubIssues: GithubIssue[]): RepoItem[] { + const repoItems: RepoItem[] = []; + + for (const githubissue of githubIssues) { + const repoItem = this.createRepoItemModel(githubissue); + repoItems.push(repoItem); + } + this.updateLocalStore(repoItems); + + return repoItems; + } + + private deleteRepoItemsFromLocalStore(ids: number[]): void { + const withoutDataToRemove = { ...this.repoItem }; + for (const id of ids) { + delete withoutDataToRemove[id]; + } + + this.repoItem = withoutDataToRemove; + + this.repoItem$.next(Object.values(this.repoItem)); + } + + /** + * Returns an array of outdated repo item ids by comparing the ids of the recently + * fetched repo items with the current repo item ids in the local store + */ + private getOutdatedRepoItemIds(fetchedRepoItemIds: number[]): number[] { + /* + Ignore for first fetch or ignore if there is no fetch result + + We also have to ignore for no fetch result as the cache might return a + 304 reponse with no differences in issues, resulting in the fetchRepoItemIds + to be empty + */ + if (this.repoItem === undefined || !fetchedRepoItemIds.length) { + return []; + } + + const fetchedRepoItemIdsSet = new Set(fetchedRepoItemIds); + + const result = Object.keys(this.repoItem) + .map((x) => +x) + .filter((issueId) => !fetchedRepoItemIdsSet.has(issueId)); + + return result; + } + + private createRepoItemModel(githubIssue: GithubIssue): RepoItem { + switch (this.viewService.currentView) { + case View.repoItemsViewer: + return this.createRepoItemInViewer(githubIssue); + default: + return; + } + } + + private createRepoItemInViewer(githubIssue: GithubIssue): RepoItem { + const type = githubIssue.issueOrPr; + switch (type) { + case 'Issue': + return Issue.createIssue(githubIssue); + case 'PullRequest': + return PullRequest.createPullRequest(githubIssue); + default: + return; + } + } + + setRepoItemTeamFilter(filterValue: string) { + if (filterValue) { + this.repoItemTeamFilter = filterValue; + } + } + + setSessionId(sessionId: string) { + this.sessionId = sessionId; + } + + getRepoItemTeamFilter(): string { + return this.repoItemTeamFilter; + } +} diff --git a/src/app/core/services/view.service.ts b/src/app/core/services/view.service.ts index 90727307d..7501e804c 100644 --- a/src/app/core/services/view.service.ts +++ b/src/app/core/services/view.service.ts @@ -17,7 +17,7 @@ export const SESSION_AVALIABILITY_FIX_FAILED = 'Session Availability Fix failed. * The title of each view that appears in the header bar. */ export const ViewDescription = { - [View.issuesViewer]: 'Issues Dashboard', + [View.repoItemsViewer]: 'Repository Items Dashboard', [View.activityDashboard]: 'Activity Dashboard' }; @@ -27,12 +27,12 @@ export const ViewDescription = { */ export const STARTING_SESSION_DATA: SessionData = { sessionRepo: [ - { view: View.issuesViewer, repos: [] } + { view: View.repoItemsViewer, repos: [] } // { view: View.activityDashboard, repos: [] } ] }; -export const STARTING_VIEW = View.issuesViewer; +export const STARTING_VIEW = View.repoItemsViewer; @Injectable({ providedIn: 'root' @@ -86,7 +86,7 @@ export class ViewService { this.sessionData.sessionRepo.find((x) => x.view === this.currentView).repos = this.getRepository(); this.githubService.storeViewDetails(this.currentRepo.owner, this.currentRepo.name); localStorage.setItem('sessionData', JSON.stringify(this.sessionData)); - this.router.navigate(['issuesViewer'], { + this.router.navigate(['repoItemsViewer'], { queryParams: { [ViewService.REPO_QUERY_PARAM_KEY]: repo.toString() }, @@ -101,7 +101,7 @@ export class ViewService { private changeCurrentRepository(repo: Repo): void { this.logger.info(`ViewService: Changing current repository to '${repo}'`); - if (this.currentView === View.issuesViewer) { + if (this.currentView === View.repoItemsViewer) { /** Adds past repositories to view */ (this.otherRepos || []).push(this.currentRepo); } @@ -180,7 +180,7 @@ export class ViewService { /** * Set items in the local storage corresponding to the next URL. * This includes checking if the view is valid, and if the repo is of the correct format. - * @param url The partial URL without the host, e.g. `/issuesViewer?repo=CATcher%2FWATcher. + * @param url The partial URL without the host, e.g. `/repoItemsViewer?repo=CATcher%2FWATcher. */ setupFromUrl(url: string): Observable { return of(this.getViewAndRepoFromUrl(url)).pipe( @@ -222,7 +222,7 @@ export class ViewService { } isViewAllowed(viewName: string) { - return viewName === '/' + View.issuesViewer; + return viewName === '/' + View.repoItemsViewer; } isRepoSet(): boolean { diff --git a/src/app/issues-viewer/issues-viewer.module.ts b/src/app/issues-viewer/issues-viewer.module.ts deleted file mode 100644 index 65dd66c85..000000000 --- a/src/app/issues-viewer/issues-viewer.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NgModule } from '@angular/core'; -import { FilterBarModule } from '../shared/filter-bar/filter-bar.module'; -import { IssuesPrCardModule } from '../shared/issue-pr-card/issue-pr-card.module'; -import { SharedModule } from '../shared/shared.module'; -import { CardViewComponent } from './card-view/card-view.component'; -import { HiddenGroupsComponent } from './hidden-groups/hidden-groups.component'; -import { IssuesViewerRoutingModule } from './issues-viewer-routing.module'; -import { IssuesViewerComponent } from './issues-viewer.component'; - -@NgModule({ - imports: [FilterBarModule, IssuesViewerRoutingModule, IssuesPrCardModule, SharedModule], - declarations: [IssuesViewerComponent, CardViewComponent, HiddenGroupsComponent], - exports: [IssuesViewerComponent, CardViewComponent] -}) -export class IssuesViewerModule {} diff --git a/src/app/issues-viewer/card-view/card-view.component.css b/src/app/repo-items-viewer/card-view/card-view.component.css similarity index 100% rename from src/app/issues-viewer/card-view/card-view.component.css rename to src/app/repo-items-viewer/card-view/card-view.component.css diff --git a/src/app/issues-viewer/card-view/card-view.component.html b/src/app/repo-items-viewer/card-view/card-view.component.html similarity index 70% rename from src/app/issues-viewer/card-view/card-view.component.html rename to src/app/repo-items-viewer/card-view/card-view.component.html index 3d2a1041f..4fc8b7b84 100644 --- a/src/app/issues-viewer/card-view/card-view.component.html +++ b/src/app/repo-items-viewer/card-view/card-view.component.html @@ -5,10 +5,10 @@ >
-
- +
+
- +
@@ -17,7 +17,7 @@ [pageSize]="pageSize" [hidePageSize]="true" [pageSizeOptions]="[10, 20, 50]" - [class]="pageSize >= issueLength ? 'pagination-hide-arrow' : ''" + [class]="pageSize >= repoItemLength ? 'pagination-hide-arrow' : ''" >
@@ -53,27 +53,27 @@
- {{ this.issues.issueCount }} + {{ this.repoItems.issueCount }}
- {{ this.issues.prCount }} + {{ this.repoItems.prCount }}
@@ -93,27 +93,27 @@
- {{ this.issues.issueCount }} + {{ this.repoItems.issueCount }}
- {{ this.issues.prCount }} + {{ this.repoItems.prCount }}
diff --git a/src/app/issues-viewer/card-view/card-view.component.ts b/src/app/repo-items-viewer/card-view/card-view.component.ts similarity index 69% rename from src/app/issues-viewer/card-view/card-view.component.ts rename to src/app/repo-items-viewer/card-view/card-view.component.ts index 30927e6bd..6b8482c01 100644 --- a/src/app/issues-viewer/card-view/card-view.component.ts +++ b/src/app/repo-items-viewer/card-view/card-view.component.ts @@ -13,15 +13,15 @@ import { 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 { RepoItem } from '../../core/models/repo-item.model'; import { AssigneeService } from '../../core/services/assignee.service'; 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 { LoggingService } from '../../core/services/logging.service'; import { MilestoneService } from '../../core/services/milestone.service'; -import { FilterableComponent, FilterableSource } from '../../shared/issue-tables/filterableTypes'; -import { IssuesDataTable } from '../../shared/issue-tables/IssuesDataTable'; +import { RepoItemService } from '../../core/services/repo-item.service'; +import { FilterableComponent, FilterableSource } from '../../shared/repo-item-tables/filterableTypes'; +import { RepoItemsDataTable } from '../../shared/repo-item-tables/RepoItemsDataTable'; @Component({ selector: 'app-card-view', @@ -30,7 +30,7 @@ import { IssuesDataTable } from '../../shared/issue-tables/IssuesDataTable'; }) /** - * Displays issues as Cards. + * Displays repo items as Cards. */ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, FilterableComponent { @Input() headers: string[]; @@ -42,26 +42,26 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt @ViewChild('assigneeHeader') assigneeHeaderTemplate: TemplateRef; @ViewChild('milestoneHeader') milestoneHeaderTemplate: TemplateRef; - issues: IssuesDataTable; - issues$: Observable; + repoItems: RepoItemsDataTable; + repoItems$: Observable; private timeoutId: NodeJS.Timeout | null = null; - private issuesLengthSubscription: Subscription; - private issuesLoadingStateSubscription: Subscription; + private repoItemsLengthSubscription: Subscription; + private repoItemsLoadingStateSubscription: Subscription; private filterSubscription: Subscription; isLoading = true; - issueLength = 0; + repoItemLength = 0; currentFilter: 'all' | 'issues' | 'prs' = 'all'; pageSize = 20; - @Output() issueLengthChange: EventEmitter = new EventEmitter(); + @Output() repoItemLengthChange: EventEmitter = new EventEmitter(); constructor( public element: ElementRef, - public issueService: IssueService, + public repoItemService: RepoItemService, public groupingContextService: GroupingContextService, private filtersService: FiltersService, private milestoneService: MilestoneService, @@ -70,8 +70,8 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt ) {} ngOnInit() { - this.issues = new IssuesDataTable( - this.issueService, + this.repoItems = new RepoItemsDataTable( + this.repoItemService, this.groupingContextService, this.filtersService, this.assigneeService, @@ -90,18 +90,18 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt ngAfterViewInit(): void { this.timeoutId = setTimeout(() => { - this.issues.loadIssues(); - this.issues$ = this.issues.connect(); - this.logger.debug('CardViewComponent: Issues loaded', this.issues$); - - // Emit event when issues change - this.issuesLengthSubscription = this.issues$.subscribe(() => { - this.issueLength = this.issues.count; - this.issueLengthChange.emit(this.issueLength); + this.repoItems.loadRepoItems(); + this.repoItems$ = this.repoItems.connect(); + this.logger.debug('CardViewComponent: Issues loaded', this.repoItems$); + + // Emit event when repo items change + this.repoItemsLengthSubscription = this.repoItems$.subscribe(() => { + this.repoItemLength = this.repoItems.count; + this.repoItemLengthChange.emit(this.repoItemLength); }); // Emit event when loading state changes - this.issuesLoadingStateSubscription = this.issues.isLoading$.subscribe((isLoadingUpdate) => { + this.repoItemsLoadingStateSubscription = this.repoItems.isLoading$.subscribe((isLoadingUpdate) => { this.isLoading = isLoadingUpdate; }); }); @@ -123,41 +123,41 @@ export class CardViewComponent implements OnInit, AfterViewInit, OnDestroy, Filt clearTimeout(this.timeoutId); } - if (this.issues) { - this.issues.disconnect(); + if (this.repoItems) { + this.repoItems.disconnect(); } - if (this.issuesLengthSubscription) { - this.issuesLengthSubscription.unsubscribe(); + if (this.repoItemsLengthSubscription) { + this.repoItemsLengthSubscription.unsubscribe(); } - if (this.issuesLoadingStateSubscription) { - this.issuesLoadingStateSubscription.unsubscribe(); + if (this.repoItemsLoadingStateSubscription) { + this.repoItemsLoadingStateSubscription.unsubscribe(); } } retrieveFilterable(): FilterableSource { - return this.issues; + return this.repoItems; } getIssueTooltip(): string { - return this.issues.issueCount + ' Issues'; + return this.repoItems.issueCount + ' Issues'; } getPrTooltip(): string { - return this.issues.prCount + ' Pull Requests'; + return this.repoItems.prCount + ' Pull Requests'; } filterByIssues(): void { const issueFilter = this.currentFilter === 'issues' ? 'all' : 'issues'; this.currentFilter = issueFilter; - this.issues.setIssueTypeFilter(issueFilter); + this.repoItems.setRepoItemTypeFilter(issueFilter); } filterByPrs(): void { const issueFilter = this.currentFilter === 'prs' ? 'all' : 'prs'; this.currentFilter = issueFilter; - this.issues.setIssueTypeFilter(issueFilter); + this.repoItems.setRepoItemTypeFilter(issueFilter); } getAssigneeTooltip(assignee: any): string { diff --git a/src/app/issues-viewer/hidden-groups/hidden-groups.component.css b/src/app/repo-items-viewer/hidden-groups/hidden-groups.component.css similarity index 100% rename from src/app/issues-viewer/hidden-groups/hidden-groups.component.css rename to src/app/repo-items-viewer/hidden-groups/hidden-groups.component.css diff --git a/src/app/issues-viewer/hidden-groups/hidden-groups.component.html b/src/app/repo-items-viewer/hidden-groups/hidden-groups.component.html similarity index 100% rename from src/app/issues-viewer/hidden-groups/hidden-groups.component.html rename to src/app/repo-items-viewer/hidden-groups/hidden-groups.component.html diff --git a/src/app/issues-viewer/hidden-groups/hidden-groups.component.ts b/src/app/repo-items-viewer/hidden-groups/hidden-groups.component.ts similarity index 100% rename from src/app/issues-viewer/hidden-groups/hidden-groups.component.ts rename to src/app/repo-items-viewer/hidden-groups/hidden-groups.component.ts diff --git a/src/app/issues-viewer/issues-viewer-routing.module.ts b/src/app/repo-items-viewer/repo-items-viewer-routing.module.ts similarity index 50% rename from src/app/issues-viewer/issues-viewer-routing.module.ts rename to src/app/repo-items-viewer/repo-items-viewer-routing.module.ts index 00305217b..571f10ce3 100644 --- a/src/app/issues-viewer/issues-viewer-routing.module.ts +++ b/src/app/repo-items-viewer/repo-items-viewer-routing.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from '../core/guards/auth.guard'; -import { IssuesViewerComponent } from './issues-viewer.component'; +import { RepoItemsViewerComponent } from './repo-items-viewer.component'; -const routes: Routes = [{ path: 'issuesViewer', component: IssuesViewerComponent, canActivate: [AuthGuard] }]; +const routes: Routes = [{ path: 'repoItemsViewer', component: RepoItemsViewerComponent, canActivate: [AuthGuard] }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) -export class IssuesViewerRoutingModule {} +export class RepoItemsViewerRoutingModule {} diff --git a/src/app/issues-viewer/issues-viewer.component.css b/src/app/repo-items-viewer/repo-items-viewer.component.css similarity index 100% rename from src/app/issues-viewer/issues-viewer.component.css rename to src/app/repo-items-viewer/repo-items-viewer.component.css diff --git a/src/app/issues-viewer/issues-viewer.component.html b/src/app/repo-items-viewer/repo-items-viewer.component.html similarity index 86% rename from src/app/issues-viewer/issues-viewer.component.html rename to src/app/repo-items-viewer/repo-items-viewer.component.html index f98b4fb9b..70aa746fd 100644 --- a/src/app/issues-viewer/issues-viewer.component.html +++ b/src/app/repo-items-viewer/repo-items-viewer.component.html @@ -8,11 +8,7 @@
- - +
@@ -30,7 +26,7 @@ *ngFor="let group of this.groupService.groups" class="issue-table" #card - [ngStyle]="{ display: card.isLoading || card.issueLength > 0 ? 'initial' : 'none' }" + [ngStyle]="{ display: card.isLoading || card.repoItemLength > 0 ? 'initial' : 'none' }" [group]="group" [headers]="this.displayedColumns" (issueLengthChange)="this.groupService.updateHiddenGroups($event, group)" diff --git a/src/app/issues-viewer/issues-viewer.component.ts b/src/app/repo-items-viewer/repo-items-viewer.component.ts similarity index 88% rename from src/app/issues-viewer/issues-viewer.component.ts rename to src/app/repo-items-viewer/repo-items-viewer.component.ts index 800e9b937..59b3b317a 100644 --- a/src/app/issues-viewer/issues-viewer.component.ts +++ b/src/app/repo-items-viewer/repo-items-viewer.component.ts @@ -6,19 +6,19 @@ import { FiltersService } from '../core/services/filters.service'; import { GithubService } from '../core/services/github.service'; import { GroupService } from '../core/services/grouping/group.service'; import { GroupingContextService } from '../core/services/grouping/grouping-context.service'; -import { IssueService } from '../core/services/issue.service'; import { LabelService } from '../core/services/label.service'; import { MilestoneService } from '../core/services/milestone.service'; +import { RepoItemService } from '../core/services/repo-item.service'; import { ViewService } from '../core/services/view.service'; -import { TABLE_COLUMNS } from '../shared/issue-tables/issue-tables-columns'; +import { TABLE_COLUMNS } from '../shared/repo-item-tables/repo-item-tables-columns'; import { CardViewComponent } from './card-view/card-view.component'; @Component({ - selector: 'app-issues-viewer', - templateUrl: './issues-viewer.component.html', - styleUrls: ['./issues-viewer.component.css'] + selector: 'app-repo-items-viewer', + templateUrl: './repo-items-viewer.component.html', + styleUrls: ['./repo-items-viewer.component.css'] }) -export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { +export class RepoItemsViewerComponent implements OnInit, AfterViewInit, OnDestroy { readonly displayedColumns = [TABLE_COLUMNS.ID, TABLE_COLUMNS.TITLE, TABLE_COLUMNS.ASSIGNEE, TABLE_COLUMNS.LABEL]; /** Observes for any change in repo*/ @@ -45,7 +45,7 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { constructor( public viewService: ViewService, public githubService: GithubService, - public issueService: IssueService, + public repoItemService: RepoItemService, public labelService: LabelService, public milestoneService: MilestoneService, public groupService: GroupService, @@ -54,7 +54,7 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { private filtersService: FiltersService ) { this.repoChangeSubscription = this.viewService.repoChanged$.subscribe((newRepo) => { - this.issueService.reset(false); + this.repoItemService.reset(false); this.labelService.reset(); this.initialize(); }); diff --git a/src/app/repo-items-viewer/repo-items-viewer.module.ts b/src/app/repo-items-viewer/repo-items-viewer.module.ts new file mode 100644 index 000000000..d138a3ffe --- /dev/null +++ b/src/app/repo-items-viewer/repo-items-viewer.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { FilterBarModule } from '../shared/filter-bar/filter-bar.module'; +import { IssuesPrCardModule } from '../shared/issue-pr-card/issue-pr-card.module'; +import { SharedModule } from '../shared/shared.module'; +import { CardViewComponent } from './card-view/card-view.component'; +import { HiddenGroupsComponent } from './hidden-groups/hidden-groups.component'; +import { RepoItemsViewerRoutingModule } from './repo-items-viewer-routing.module'; +import { RepoItemsViewerComponent } from './repo-items-viewer.component'; + +@NgModule({ + imports: [FilterBarModule, RepoItemsViewerRoutingModule, IssuesPrCardModule, SharedModule], + declarations: [RepoItemsViewerComponent, CardViewComponent, HiddenGroupsComponent], + exports: [RepoItemsViewerComponent, CardViewComponent] +}) +export class RepoItemsViewerModule {} diff --git a/src/app/shared/filter-bar/filter-bar.component.ts b/src/app/shared/filter-bar/filter-bar.component.ts index 87387a576..ac8c34345 100644 --- a/src/app/shared/filter-bar/filter-bar.component.ts +++ b/src/app/shared/filter-bar/filter-bar.component.ts @@ -21,7 +21,7 @@ import { GroupBy, GroupingContextService } from '../../core/services/grouping/gr import { LoggingService } from '../../core/services/logging.service'; import { MilestoneService } from '../../core/services/milestone.service'; import { ViewService } from '../../core/services/view.service'; -import { FilterableComponent } from '../issue-tables/filterableTypes'; +import { FilterableComponent } from '../repo-item-tables/filterableTypes'; import { LabelFilterBarComponent } from './label-filter-bar/label-filter-bar.component'; /** @@ -91,7 +91,7 @@ export class FilterBarComponent implements OnInit, OnDestroy { } /** - * Signals to IssuesDataTable that a change has occurred in filter. + * Signals to RepoItemsDataTable that a change has occurred in filter. */ applyFilter() { this.views$?.value?.forEach((v) => (v.retrieveFilterable().filter = this.filter)); @@ -163,4 +163,3 @@ export class FilterBarComponent implements OnInit, OnDestroy { event.stopImmediatePropagation(); } } - diff --git a/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.html b/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.html index 42d297da2..655aac469 100644 --- a/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.html +++ b/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.html @@ -1,6 +1,6 @@ - - #{{ issue.id }}: {{ fitTitleText() }} + + #{{ repoItem.id }}: {{ fitTitleText() }} diff --git a/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.ts b/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.ts index 15ed34151..5ccaec011 100644 --- a/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.ts +++ b/src/app/shared/issue-pr-card/issue-pr-card-header/issue-pr-card-header.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { Issue } from '../../../core/models/issue.model'; +import { RepoItem } from '../../../core/models/repo-item.model'; @Component({ selector: 'app-issue-pr-card-header', @@ -7,7 +7,7 @@ import { Issue } from '../../../core/models/issue.model'; styleUrls: ['./issue-pr-card-header.component.css'] }) export class IssuePrCardHeaderComponent { - @Input() issue: Issue; + @Input() repoItem: RepoItem; constructor() {} @@ -16,9 +16,9 @@ export class IssuePrCardHeaderComponent { * @returns string to create icon */ getOcticon() { - const type = this.issue.issueOrPr; - const state = this.issue.state; - const stateReason = this.issue.stateReason; + const type = this.repoItem.constructor.name; + const state = this.repoItem.state; + const stateReason = this.repoItem.stateReason; if (type === 'Issue') { if (state === 'OPEN') { @@ -32,7 +32,7 @@ export class IssuePrCardHeaderComponent { } } else if (type === 'PullRequest') { if (state === 'OPEN') { - if (this.issue.isDraft) { + if (this.repoItem.isDraft) { return 'git-pull-request-draft'; } return 'git-pull-request'; @@ -46,17 +46,17 @@ export class IssuePrCardHeaderComponent { } } - /** Returns status color for issue */ - getIssueOpenOrCloseColor() { - if (this.issue.state === 'OPEN') { - if (this.issue.isDraft) { + /** Returns status color for repoItem */ + getRepoItemOpenOrCloseColor() { + if (this.repoItem.state === 'OPEN') { + if (this.repoItem.isDraft) { return 'grey'; } else { return 'green'; } - } else if (this.issue.issueOrPr === 'PullRequest' && this.issue.state === 'CLOSED') { + } else if (this.repoItem.type === 'PullRequest' && this.repoItem.state === 'CLOSED') { return 'red'; - } else if (this.issue.issueOrPr === 'Issue' && this.issue.stateReason === 'NOT_PLANNED') { + } else if (this.repoItem.type === 'Issue' && this.repoItem.stateReason === 'NOT_PLANNED') { return 'gray'; } else { return 'purple'; @@ -73,7 +73,7 @@ export class IssuePrCardHeaderComponent { const SPLITTER_TEXT = ' '; const ELLIPSES = '...'; - return this.issue.title + return this.repoItem.title .split(SPLITTER_TEXT) .map((word) => { if (word.length > MAX_WORD_LENGTH) { diff --git a/src/app/shared/issue-pr-card/issue-pr-card-review-decision/issue-pr-card-review-decision.component.ts b/src/app/shared/issue-pr-card/issue-pr-card-review-decision/issue-pr-card-review-decision.component.ts index 294a14fae..02831bdf4 100644 --- a/src/app/shared/issue-pr-card/issue-pr-card-review-decision/issue-pr-card-review-decision.component.ts +++ b/src/app/shared/issue-pr-card/issue-pr-card-review-decision/issue-pr-card-review-decision.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ReviewDecision } from '../../../core/models/issue.model'; +import { ReviewDecision } from '../../../core/models/repo-item.model'; @Component({ selector: 'app-issue-pr-card-review-decision', diff --git a/src/app/shared/issue-pr-card/issue-pr-card.component.html b/src/app/shared/issue-pr-card/issue-pr-card.component.html index f39abef1d..9ddd40db5 100644 --- a/src/app/shared/issue-pr-card/issue-pr-card.component.html +++ b/src/app/shared/issue-pr-card/issue-pr-card.component.html @@ -1,32 +1,32 @@ - + - - + +
- +
diff --git a/src/app/shared/issue-pr-card/issue-pr-card.component.ts b/src/app/shared/issue-pr-card/issue-pr-card.component.ts index 0761298d1..3ab050b77 100644 --- a/src/app/shared/issue-pr-card/issue-pr-card.component.ts +++ b/src/app/shared/issue-pr-card/issue-pr-card.component.ts @@ -1,5 +1,6 @@ import { Component, Input } from '@angular/core'; -import { Issue } from '../../core/models/issue.model'; +import { PullRequest } from '../../core/models/pull-request.model'; +import { RepoItem } from '../../core/models/repo-item.model'; import { Filter } from '../../core/services/filters.service'; import { GithubService } from '../../core/services/github.service'; import { LabelService } from '../../core/services/label.service'; @@ -12,7 +13,7 @@ import { MilestoneService } from '../../core/services/milestone.service'; styleUrls: ['./issue-pr-card.component.css'] }) export class IssuePrCardComponent { - @Input() issue: Issue; + @Input() repoItem: RepoItem; @Input() filter?: Filter; constructor( @@ -22,39 +23,35 @@ export class IssuePrCardComponent { public milestoneService: MilestoneService ) {} - isIssue(): boolean { - return this.issue.issueOrPr === 'Issue'; - } - isNotFollowingForkingWorkflow() { return ( - this.issue.issueOrPr === 'PullRequest' && this.issue.headRepository?.toLowerCase() === this.githubService.getRepoURL().toLowerCase() + this.isPullRequest(this.repoItem) && this.repoItem.headRepository?.toLowerCase() === this.githubService.getRepoURL().toLowerCase() ); } - /** Opens issue in new window */ + /** Opens repo item in new window */ viewIssueInBrowser(event: Event) { - this.logger.info(`CardViewComponent: Opening Issue ${this.issue.id} on Github`); - this.githubService.viewIssueInBrowser(this.issue.id, event); + this.logger.info(`CardViewComponent: Opening Issue ${this.repoItem.id} on Github`); + this.githubService.viewIssueInBrowser(this.repoItem.id, event); } /** Opens milestone in new window */ viewMilestoneInBrowser(event: Event) { - this.logger.info(`CardViewComponent: Opening Milestone ${this.issue.milestone.number} on Github`); - this.githubService.viewMilestoneInBrowser(this.issue.milestone.number, event); + this.logger.info(`CardViewComponent: Opening Milestone ${this.repoItem.milestone.number} on Github`); + this.githubService.viewMilestoneInBrowser(this.repoItem.milestone.number, event); } /** Returns CSS class for border color */ getIssueOpenOrCloseColorCSSClass() { - if (this.issue.state === 'OPEN') { - if (this.issue.isDraft) { + if (this.repoItem.state === 'OPEN') { + if (this.repoItem.isDraft) { return 'border-gray'; } else { return 'border-green'; } - } else if (this.issue.issueOrPr === 'PullRequest' && this.issue.state === 'CLOSED') { + } else if (this.isPullRequest(this.repoItem) && this.repoItem.state === 'CLOSED') { return 'border-red'; - } else if (this.issue.issueOrPr === 'Issue' && this.issue.stateReason === 'NOT_PLANNED') { + } else if (this.isIssue(this.repoItem) && this.repoItem.stateReason === 'NOT_PLANNED') { return 'border-gray'; } else { return 'border-purple'; @@ -63,17 +60,25 @@ export class IssuePrCardComponent { /** * Truncates description to fit in card content. - * @param description - Description of Issue that is to be displayed. + * @param description - Description of Repo Item that is to be displayed. */ fitDescriptionText(): string { // Arbitrary Length of Characters beyond which an overflow occurs. const MAX_CHARACTER_LENGTH = 72; const ELLIPSES = '...'; - return this.issue.description.slice(0, MAX_CHARACTER_LENGTH) + ELLIPSES; + return this.repoItem.description.slice(0, MAX_CHARACTER_LENGTH) + ELLIPSES; + } + + isMergedWithoutReview(repoItem: RepoItem): boolean { + return this.isPullRequest(repoItem) && repoItem.state === 'MERGED' && (!repoItem.reviews || repoItem.reviews.length === 0); + } + + isPullRequest(repoItem: RepoItem): boolean { + return repoItem.type === 'PullRequest'; } - isMergedWithoutReview(issue: Issue): boolean { - return issue.issueOrPr === 'PullRequest' && issue.state === 'MERGED' && (!issue.reviews || issue.reviews.length === 0); + isIssue(repoItem: RepoItem): boolean { + return repoItem.type === 'Issue'; } } diff --git a/src/app/shared/layout/header.component.ts b/src/app/shared/layout/header.component.ts index d8a60e078..0543b5a49 100644 --- a/src/app/shared/layout/header.component.ts +++ b/src/app/shared/layout/header.component.ts @@ -15,9 +15,9 @@ import { FiltersService } from '../../core/services/filters.service'; import { GithubService } from '../../core/services/github.service'; import { GithubEventService } from '../../core/services/githubevent.service'; import { GroupingContextService } from '../../core/services/grouping/grouping-context.service'; -import { IssueService } from '../../core/services/issue.service'; import { LabelService } from '../../core/services/label.service'; import { LoggingService } from '../../core/services/logging.service'; +import { RepoItemService } from '../../core/services/repo-item.service'; 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'; @@ -67,7 +67,7 @@ export class HeaderComponent implements OnInit { public repoUrlCacheService: RepoUrlCacheService, private location: Location, private githubEventService: GithubEventService, - private issueService: IssueService, + private repoItemService: RepoItemService, private labelService: LabelService, private errorHandlingService: ErrorHandlingService, private githubService: GithubService, @@ -105,7 +105,7 @@ export class HeaderComponent implements OnInit { this.initializeRepoNameInTitle(); }); - this.isLoading$ = this.issueService.isLoading.asObservable(); + this.isLoading$ = this.repoItemService.isLoading.asObservable(); } ngOnInit() {} @@ -124,9 +124,9 @@ export class HeaderComponent implements OnInit { // Replace Current View Data. this.viewService.changeView(View[selectedView]); - // Remove current view issues and load selected view issues. + // Remove current view issues and load selected view repo items. this.githubService.reset(); - this.issueService.reset(false); + this.repoItemService.reset(false); this.labelService.reset(); this.reload(); @@ -143,7 +143,7 @@ export class HeaderComponent implements OnInit { } isOpenUrlButtonShown(): boolean { - return this.viewService.currentView === View.issuesViewer || this.viewService.currentView === View.activityDashboard; + return this.viewService.currentView === View.repoItemsViewer || this.viewService.currentView === View.activityDashboard; } getVersion(): string { diff --git a/src/app/shared/issue-tables/IssuesDataTable.ts b/src/app/shared/repo-item-tables/RepoItemsDataTable.ts similarity index 60% rename from src/app/shared/issue-tables/IssuesDataTable.ts rename to src/app/shared/repo-item-tables/RepoItemsDataTable.ts index 147e82800..ab055af86 100644 --- a/src/app/shared/issue-tables/IssuesDataTable.ts +++ b/src/app/shared/repo-item-tables/RepoItemsDataTable.ts @@ -4,31 +4,31 @@ import { BehaviorSubject, merge, Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { GithubUser } from '../../core/models/github-user.model'; import { Group } from '../../core/models/github/group.interface'; -import { Issue } from '../../core/models/issue.model'; import { Milestone } from '../../core/models/milestone.model'; +import { RepoItem } from '../../core/models/repo-item.model'; import { AssigneeService } from '../../core/services/assignee.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 { MilestoneService } from '../../core/services/milestone.service'; +import { RepoItemService } from '../../core/services/repo-item.service'; import { applyDropdownFilter } from './dropdownfilter'; import { FilterableSource } from './filterableTypes'; -import { paginateData } from './issue-paginator'; -import { applySort } from './issue-sorter'; +import { paginateData } from './repo-item-paginator'; +import { applySort } from './repo-item-sorter'; import { applySearchFilter } from './search-filter'; -export class IssuesDataTable extends DataSource implements FilterableSource { +export class RepoItemsDataTable extends DataSource implements FilterableSource { public count = 0; public issueCount = 0; public prCount = 0; public hasIssue = false; public hasPR = false; private filterChange = new BehaviorSubject(this.filtersService.defaultFilter); - private issuesSubject = new BehaviorSubject([]); - private issueSubscription: Subscription; - private issueTypeFilter: 'all' | 'issues' | 'prs' = 'all'; // initialise as 'all' + private repoItemsSubject = new BehaviorSubject([]); + private repoItemSubscription: Subscription; + private repoItemTypeFilter: 'all' | 'issues' | 'prs' = 'all'; // initialise as 'all' - public isLoading$ = this.issueService.isLoading.asObservable(); + public isLoading$ = this.repoItemService.isLoading.asObservable(); private static isGroupInFilter(group: Group, filter: Filter): boolean { const groupFilterAsGithubUser = filter.assignees.map((selectedAssignee) => { @@ -46,7 +46,7 @@ export class IssuesDataTable extends DataSource implements FilterableSour } constructor( - private issueService: IssueService, + private repoItemService: RepoItemService, private groupingContextService: GroupingContextService, private filtersService: FiltersService, private assigneeService: AssigneeService, @@ -54,52 +54,52 @@ export class IssuesDataTable extends DataSource implements FilterableSour private paginator: MatPaginator, private displayedColumn: string[], private group?: Group, - private defaultFilter?: (issue: Issue) => boolean + private defaultFilter?: (repoItem: RepoItem) => boolean ) { super(); } - connect(): Observable { - return this.issuesSubject.asObservable(); + connect(): Observable { + return this.repoItemsSubject.asObservable(); } disconnect() { this.filterChange.complete(); - this.issuesSubject.complete(); - if (this.issueSubscription) { - this.issueSubscription.unsubscribe(); + this.repoItemsSubject.complete(); + if (this.repoItemSubscription) { + this.repoItemSubscription.unsubscribe(); } - this.issueService.stopPollIssues(); + this.repoItemService.stopPollRepoItems(); } - setIssueTypeFilter(filter: 'all' | 'issues' | 'prs') { - this.issueTypeFilter = filter; - this.loadIssues(); + setRepoItemTypeFilter(filter: 'all' | 'issues' | 'prs') { + this.repoItemTypeFilter = filter; + this.loadRepoItems(); } - getIssueTypeFilter(): 'all' | 'issues' | 'prs' { - return this.issueTypeFilter; + getRepoItemTypeFilter(): 'all' | 'issues' | 'prs' { + return this.repoItemTypeFilter; } - loadIssues() { + loadRepoItems() { let page; if (this.paginator !== undefined) { page = this.paginator.page; } - const displayDataChanges = [this.issueService.issues$, page, this.filterChange].filter((x) => x !== undefined); + const displayDataChanges = [this.repoItemService.repoItem$, page, this.filterChange].filter((x) => x !== undefined); - this.issueService.startPollIssues(); - this.issueSubscription = merge(...displayDataChanges) + this.repoItemService.startPollRepoItems(); + this.repoItemSubscription = merge(...displayDataChanges) .pipe( - // maps each change in display value to new issue ordering or filtering + // maps each change in display value to new repo item ordering or filtering map(() => { - if (!IssuesDataTable.isGroupInFilter(this.group, this.filter)) { + if (!RepoItemsDataTable.isGroupInFilter(this.group, this.filter)) { this.count = 0; return []; } - let data = Object.values(this.issueService.issues$.getValue()).reverse(); + let data = Object.values(this.repoItemService.repoItem$.getValue()).reverse(); if (this.defaultFilter) { data = data.filter(this.defaultFilter); } @@ -109,20 +109,20 @@ export class IssuesDataTable extends DataSource implements FilterableSour // Apply Filters data = applyDropdownFilter(this.filter, data, !this.milestoneService.hasNoMilestones, !this.assigneeService.hasNoAssignees); - data = applySearchFilter(this.filter.title, this.displayedColumn, this.issueService, data); + data = applySearchFilter(this.filter.title, this.displayedColumn, this.repoItemService, data); + this.issueCount = data.filter((datum) => datum.type === 'Issue').length; + this.prCount = data.filter((datum) => datum.type === 'PullRequest').length; - this.issueCount = data.filter((issue) => issue.issueOrPr !== 'PullRequest').length; - this.prCount = data.filter((issue) => issue.issueOrPr === 'PullRequest').length; this.hasIssue = this.issueCount > 0; this.hasPR = this.prCount > 0; // Apply Issue Type Filter for header component - if (this.issueTypeFilter !== 'all') { - const issueType = this.issueTypeFilter === 'issues' ? 'Issue' : 'PullRequest'; - const filteredData = data.filter((issue) => issue.issueOrPr === issueType); + if (this.repoItemTypeFilter !== 'all') { + const issueType = this.repoItemTypeFilter === 'issues' ? 'Issue' : 'PullRequest'; + const filteredData = data.filter((datum) => datum.type === issueType); if (filteredData.length === 0) { - this.issueTypeFilter = 'all'; + this.repoItemTypeFilter = 'all'; } else { data = filteredData; } @@ -138,8 +138,8 @@ export class IssuesDataTable extends DataSource implements FilterableSour return data; }) ) - .subscribe((issues) => { - this.issuesSubject.next(issues); + .subscribe((items) => { + this.repoItemsSubject.next(items); }); } diff --git a/src/app/shared/issue-tables/dropdownfilter.ts b/src/app/shared/repo-item-tables/dropdownfilter.ts similarity index 55% rename from src/app/shared/issue-tables/dropdownfilter.ts rename to src/app/shared/repo-item-tables/dropdownfilter.ts index 47cd5eae7..1417ab6e9 100644 --- a/src/app/shared/issue-tables/dropdownfilter.ts +++ b/src/app/shared/repo-item-tables/dropdownfilter.ts @@ -1,4 +1,4 @@ -import { Issue } from '../../core/models/issue.model'; +import { RepoItem } from '../../core/models/repo-item.model'; import { Filter } from '../../core/services/filters.service'; type StatusInfo = { @@ -15,18 +15,18 @@ const infoFromStatus = (statusString: string): StatusInfo => { }; /** - * This module serves to improve separation of concerns in IssuesDataTable.ts and IssueList.ts module by containing the logic for + * This module serves to improve separation of concerns in RepoItemsDataTable.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 the data passed in after all the filters of dropdownFilters are applied */ export function applyDropdownFilter( filter: Filter, - data: Issue[], + data: RepoItem[], isFilteringByMilestone: boolean, isFilteringByAssignee: boolean -): Issue[] { - const filteredData: Issue[] = data.filter((issue) => { +): RepoItem[] { + const filteredData: RepoItem[] = data.filter((datum) => { let ret = true; // status can either be 'open', 'closed', or 'merged' @@ -34,32 +34,32 @@ export function applyDropdownFilter( ret && filter.status.some((item) => { const statusInfo = infoFromStatus(item); - return statusInfo.status === issue.state.toLowerCase() && statusInfo.type === issue.issueOrPr.toLowerCase(); + return statusInfo.status === datum.state.toLowerCase() && statusInfo.type === datum.constructor.name.toLowerCase(); }); if (filter.type === 'issue') { - ret = ret && issue.issueOrPr === 'Issue'; + ret = ret && datum.type === 'Issue'; } else if (filter.type === 'pullrequest') { - ret = ret && issue.issueOrPr === 'PullRequest'; + ret = ret && datum.type === 'PullRequest'; } - ret = ret && (!isFilteringByMilestone || filter.milestones.some((milestone) => issue.milestone.title === milestone)); - ret = ret && (!isFilteringByAssignee || isFilteredByAssignee(filter, issue)); - ret = ret && issue.labels.every((label) => !filter.deselectedLabels.has(label)); - return ret && filter.labels.every((label) => issue.labels.includes(label)); + ret = ret && (!isFilteringByMilestone || filter.milestones.some((milestone) => datum.milestone.title === milestone)); + ret = ret && (!isFilteringByAssignee || isFilteredByAssignee(filter, datum)); + ret = ret && datum.labels.every((label) => !filter.deselectedLabels.has(label)); + return ret && filter.labels.every((label) => datum.labels.includes(label)); }); return filteredData; } -function isFilteredByAssignee(filter: Filter, issue: Issue): boolean { - if (issue.issueOrPr === 'Issue') { +function isFilteredByAssignee(filter: Filter, data: RepoItem): boolean { + if (data.type === 'Issue') { return ( - filter.assignees.some((assignee) => issue.assignees.includes(assignee)) || - (filter.assignees.includes('Unassigned') && issue.assignees.length === 0) + filter.assignees.some((assignee) => data.assignees.includes(assignee)) || + (filter.assignees.includes('Unassigned') && data.assignees.length === 0) ); - } else if (issue.issueOrPr === 'PullRequest') { + } else if (data.type === 'PullRequest') { return ( - filter.assignees.some((assignee) => issue.author === assignee) || (filter.assignees.includes('Unassigned') && issue.author === null) + filter.assignees.some((assignee) => data.author === assignee) || (filter.assignees.includes('Unassigned') && data.author === null) ); // note that issue.author is never == null for PRs, but is left for semantic reasons } else { diff --git a/src/app/shared/issue-tables/filterableTypes.ts b/src/app/shared/repo-item-tables/filterableTypes.ts similarity index 100% rename from src/app/shared/issue-tables/filterableTypes.ts rename to src/app/shared/repo-item-tables/filterableTypes.ts diff --git a/src/app/shared/issue-tables/issue-paginator.ts b/src/app/shared/repo-item-tables/repo-item-paginator.ts similarity index 73% rename from src/app/shared/issue-tables/issue-paginator.ts rename to src/app/shared/repo-item-tables/repo-item-paginator.ts index 6bf13824a..168ddbe9f 100644 --- a/src/app/shared/issue-tables/issue-paginator.ts +++ b/src/app/shared/repo-item-tables/repo-item-paginator.ts @@ -1,7 +1,7 @@ import { MatPaginator } from '@angular/material/paginator'; -import { Issue } from '../../core/models/issue.model'; +import { RepoItem } from '../../core/models/repo-item.model'; -export function paginateData(paginator: MatPaginator, data: Issue[]): Issue[] { +export function paginateData(paginator: MatPaginator, data: RepoItem[]): RepoItem[] { paginator.length = data.length; let result = getDataForPage(paginator.pageIndex, paginator.pageSize, data); if (result.length === 0) { @@ -11,7 +11,7 @@ export function paginateData(paginator: MatPaginator, data: Issue[]): Issue[] { return result; } -function getDataForPage(pageIndex: number, pageSize: number, data: Issue[]): Issue[] { +function getDataForPage(pageIndex: number, pageSize: number, data: RepoItem[]): RepoItem[] { const startIndex = pageIndex * pageSize; return data.splice(startIndex, pageSize); } diff --git a/src/app/shared/issue-tables/issue-sorter.ts b/src/app/shared/repo-item-tables/repo-item-sorter.ts similarity index 75% rename from src/app/shared/issue-tables/issue-sorter.ts rename to src/app/shared/repo-item-tables/repo-item-sorter.ts index bcbd5d8f0..324defbad 100644 --- a/src/app/shared/issue-tables/issue-sorter.ts +++ b/src/app/shared/repo-item-tables/repo-item-sorter.ts @@ -1,8 +1,8 @@ import { Sort } from '@angular/material/sort'; import * as moment from 'moment'; -import { Issue } from '../../core/models/issue.model'; +import { RepoItem } from '../../core/models/repo-item.model'; -export function applySort(sort: Sort, data: Issue[]): Issue[] { +export function applySort(sort: Sort, data: RepoItem[]): RepoItem[] { if (!sort.active) { return data; } @@ -15,7 +15,7 @@ export function applySort(sort: Sort, data: Issue[]): Issue[] { case 'date': return data.sort((a, b) => direction * compareByDateValue(a.updated_at, b.updated_at)); case 'status': - return data.sort((a, b) => direction * compareByIssueType(a, b)); + return data.sort((a, b) => direction * compareByRepoItemType(a, b)); default: // title, responseTag are string values return data.sort((a, b) => direction * compareByStringValue(a[sort.active], b[sort.active])); @@ -36,7 +36,7 @@ function compareByDateValue(valueA: string, valueB: string): number { return moment(valueA).isBefore(valueB) ? -1 : 1; } -function compareByIssueType(valueA: Issue, valueB: Issue): number { +function compareByRepoItemType(valueA: RepoItem, valueB: RepoItem): number { const sortOrder = { 'OPEN PullRequest': 0, 'OPEN Issue': 1, @@ -45,8 +45,8 @@ function compareByIssueType(valueA: Issue, valueB: Issue): number { 'CLOSED PullRequest': 4 }; - const aOrder = sortOrder[valueA.state + ' ' + valueA.issueOrPr] || -1; - const bOrder = sortOrder[valueB.state + ' ' + valueB.issueOrPr] || -1; + const aOrder = sortOrder[valueA.state + ' ' + valueA.constructor.name] || -1; + const bOrder = sortOrder[valueB.state + ' ' + valueB.constructor.name] || -1; if (aOrder === bOrder) { return compareByStringValue(valueA.title, valueB.title); diff --git a/src/app/shared/issue-tables/issue-tables-columns.ts b/src/app/shared/repo-item-tables/repo-item-tables-columns.ts similarity index 100% rename from src/app/shared/issue-tables/issue-tables-columns.ts rename to src/app/shared/repo-item-tables/repo-item-tables-columns.ts diff --git a/src/app/shared/issue-tables/search-filter.ts b/src/app/shared/repo-item-tables/search-filter.ts similarity index 55% rename from src/app/shared/issue-tables/search-filter.ts rename to src/app/shared/repo-item-tables/search-filter.ts index faf951776..bc3a07767 100644 --- a/src/app/shared/issue-tables/search-filter.ts +++ b/src/app/shared/repo-item-tables/search-filter.ts @@ -1,32 +1,32 @@ -import { Issue } from '../../core/models/issue.model'; -import { IssueService } from '../../core/services/issue.service'; -import { TABLE_COLUMNS } from './issue-tables-columns'; +import { RepoItem } from '../../core/models/repo-item.model'; +import { RepoItemService } from '../../core/services/repo-item.service'; +import { TABLE_COLUMNS } from './repo-item-tables-columns'; /** - * This module serves to improve separation of concerns in IssuesDataTable.ts module by containing the logic for - * applying search filter to the issues data table in this module. + * This module serves to improve separation of concerns in RepoItemsDataTable.ts module by containing the logic for + * applying search filter to the repo items data table in this module. * This module exports a 2 function applySearchFilter and searchFilter - * SearchFilter returns a function to test if an Issue matches - * applySearchFilter applies searchfilter to a list of issues. + * SearchFilter returns a function to test if a RepoItem matches + * applySearchFilter applies searchfilter to a list of repo items. */ -export function searchFilter(filter: string, displayedColumn: string[]): (a: Issue) => boolean { +export function searchFilter(filter: string, displayedColumn: string[]): (a: RepoItem) => boolean { const searchKey = filter.toLowerCase(); - return (issue: Issue) => { + return (data: RepoItem) => { for (const column of displayedColumn) { switch (column) { case TABLE_COLUMNS.LABEL: - if (matchesLabel(issue.labels, searchKey)) { + if (matchesLabel(data.labels, searchKey)) { return true; } break; case TABLE_COLUMNS.ASSIGNEE: - if (matchesAssignee(issue.assignees, searchKey)) { + if (matchesAssignee(data.assignees, searchKey)) { return true; } break; default: - if (matchesOtherColumns(issue, column, searchKey)) { + if (matchesOtherColumns(data, column, searchKey)) { return true; } break; @@ -36,7 +36,12 @@ export function searchFilter(filter: string, displayedColumn: string[]): (a: Iss }; } -export function applySearchFilter(filter: string, displayedColumn: string[], issueService: IssueService, data: Issue[]): Issue[] { +export function applySearchFilter( + filter: string, + displayedColumn: string[], + repoItemService: RepoItemService, + data: RepoItem[] +): RepoItem[] { const result = data.slice().filter(searchFilter(filter, displayedColumn)); return result; } @@ -63,7 +68,7 @@ function matchesLabel(labels: string[], searchKey: string): boolean { } } -function matchesOtherColumns(issue: Issue, column: string, searchKey: string): boolean { - const searchStr = String(issue[column]).toLowerCase(); +function matchesOtherColumns(data: RepoItem, column: string, searchKey: string): boolean { + const searchStr = String(data[column]).toLowerCase(); return containsSearchKey(searchStr, searchKey); } diff --git a/tests/app/core/models/session-model.spec.ts b/tests/app/core/models/session-model.spec.ts index 4a5b493f2..eb93ce68c 100644 --- a/tests/app/core/models/session-model.spec.ts +++ b/tests/app/core/models/session-model.spec.ts @@ -51,25 +51,25 @@ describe('Session Model', () => { }); it('should throw error on session data with invalid repo', () => { - of({ sessionRepo: [{ view: View.issuesViewer, repo: undefined }] }) + of({ sessionRepo: [{ view: View.repoItemsViewer, repo: undefined }] }) .pipe(assertSessionDataIntegrity()) .subscribe({ next: () => fail(), error: (err) => expect(err).toEqual(new Error(OPENED_VIEW_REPO_UNDEFINED)) }); - of({ sessionRepo: [{ view: View.issuesViewer, repo: null }] }) + of({ sessionRepo: [{ view: View.repoItemsViewer, repo: null }] }) .pipe(assertSessionDataIntegrity()) .subscribe({ next: () => fail(), error: (err) => expect(err).toEqual(new Error(OPENED_VIEW_REPO_UNDEFINED)) }); - of({ sessionRepo: [{ view: View.issuesViewer, repo: '' }] }) + of({ sessionRepo: [{ view: View.repoItemsViewer, repo: '' }] }) .pipe(assertSessionDataIntegrity()) .subscribe({ next: () => fail(), error: (err) => expect(err).toEqual(new Error(OPENED_VIEW_REPO_UNDEFINED)) }); - of({ sessionRepo: [{ view: View.issuesViewer, repo: [] }] }) + of({ sessionRepo: [{ view: View.repoItemsViewer, repo: [] }] }) .pipe(assertSessionDataIntegrity()) .subscribe({ next: () => fail(), diff --git a/tests/app/shared/issue-tables/issue-paginator.spec.ts b/tests/app/shared/repo-item-tables/repo-item-paginator.spec.ts similarity index 80% rename from tests/app/shared/issue-tables/issue-paginator.spec.ts rename to tests/app/shared/repo-item-tables/repo-item-paginator.spec.ts index e4f0ce4e3..82115c5c8 100644 --- a/tests/app/shared/issue-tables/issue-paginator.spec.ts +++ b/tests/app/shared/repo-item-tables/repo-item-paginator.spec.ts @@ -1,6 +1,6 @@ import { MatPaginator } from '@angular/material/paginator'; import { Issue } from '../../../../src/app/core/models/issue.model'; -import { paginateData } from '../../../../src/app/shared/issue-tables/issue-paginator'; +import { paginateData } from '../../../../src/app/shared/repo-item-tables/repo-item-paginator'; import { ISSUE_WITH_ASSIGNEES, ISSUE_WITH_EMPTY_DESCRIPTION, @@ -12,10 +12,10 @@ describe('issue-paginator', () => { describe('paginateData()', () => { let dataSet_7: Issue[]; let paginator: MatPaginator; - const mediumSeverityIssueWithResponse: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION); - const mediumSeverityIssueWithAssigneee: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_ASSIGNEES); - const lowSeverityFeatureFlawIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION_LOW_SEVERITY); - const highSeverityDocumentationBugIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION_HIGH_SEVERITY); + const mediumSeverityIssueWithResponse: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION); + const mediumSeverityIssueWithAssigneee: Issue = Issue.createIssue(ISSUE_WITH_ASSIGNEES); + const lowSeverityFeatureFlawIssue: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION_LOW_SEVERITY); + const highSeverityDocumentationBugIssue: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION_HIGH_SEVERITY); beforeEach(() => { dataSet_7 = [ diff --git a/tests/app/shared/issue-tables/issue-sorter.spec.ts b/tests/app/shared/repo-item-tables/repo-item-sorter.spec.ts similarity index 84% rename from tests/app/shared/issue-tables/issue-sorter.spec.ts rename to tests/app/shared/repo-item-tables/repo-item-sorter.spec.ts index 79c047927..60fd48cb7 100644 --- a/tests/app/shared/issue-tables/issue-sorter.spec.ts +++ b/tests/app/shared/repo-item-tables/repo-item-sorter.spec.ts @@ -1,6 +1,6 @@ import { MatSort } from '@angular/material/sort'; import { Issue } from '../../../../src/app/core/models/issue.model'; -import { applySort } from '../../../../src/app/shared/issue-tables/issue-sorter'; +import { applySort } from '../../../../src/app/shared/repo-item-tables/repo-item-sorter'; import { ISSUE_UPDATED_EARLIER, ISSUE_UPDATED_LATER, @@ -10,12 +10,12 @@ import { describe('issuer-sorter', () => { describe('applySort()', () => { - const dummyIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION); - const otherDummyIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_ASSIGNEES); + const dummyIssue: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION); + const otherDummyIssue: Issue = Issue.createIssue(ISSUE_WITH_ASSIGNEES); const issuesList: Issue[] = [dummyIssue, otherDummyIssue]; - const issueUpdatedEarlier: Issue = Issue.createPhaseBugReportingIssue(ISSUE_UPDATED_EARLIER); - const issueUpdatedLater: Issue = Issue.createPhaseBugReportingIssue(ISSUE_UPDATED_LATER); + const issueUpdatedEarlier: Issue = Issue.createIssue(ISSUE_UPDATED_EARLIER); + const issueUpdatedLater: Issue = Issue.createIssue(ISSUE_UPDATED_LATER); const issuesWithDifferentUpdatedDate: Issue[] = [issueUpdatedEarlier, issueUpdatedLater]; const matSort: MatSort = new MatSort(); diff --git a/tests/app/shared/issue-tables/search-filter.spec.ts b/tests/app/shared/repo-item-tables/search-filter.spec.ts similarity index 55% rename from tests/app/shared/issue-tables/search-filter.spec.ts rename to tests/app/shared/repo-item-tables/search-filter.spec.ts index 3b520725d..18161f4a6 100644 --- a/tests/app/shared/issue-tables/search-filter.spec.ts +++ b/tests/app/shared/repo-item-tables/search-filter.spec.ts @@ -1,7 +1,7 @@ import { Issue } from '../../../../src/app/core/models/issue.model'; -import { IssueService } from '../../../../src/app/core/services/issue.service'; -import { TABLE_COLUMNS } from '../../../../src/app/shared/issue-tables/issue-tables-columns'; -import { applySearchFilter } from '../../../../src/app/shared/issue-tables/search-filter'; +import { RepoItemService } from '../../../../src/app/core/services/repo-item.service'; +import { TABLE_COLUMNS } from '../../../../src/app/shared/repo-item-tables/repo-item-tables-columns'; +import { applySearchFilter } from '../../../../src/app/shared/repo-item-tables/search-filter'; import { USER_ANUBHAV } from '../../../constants/data.constants'; import { ISSUE_WITH_ASSIGNEES, @@ -14,10 +14,10 @@ import { GITHUB_LABEL_FEATURE_FLAW } from '../../../constants/githublabel.consta describe('search-filter', () => { describe('applySearchFilter()', () => { let searchKey: string; - const mediumSeverityIssueWithResponse: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION); - const mediumSeverityIssueWithAssigneee: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_ASSIGNEES); - const lowSeverityFeatureFlawIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION_LOW_SEVERITY); - const highSeverityDocumentationBugIssue: Issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION_HIGH_SEVERITY); + const mediumSeverityIssueWithResponse: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION); + const mediumSeverityIssueWithAssigneee: Issue = Issue.createIssue(ISSUE_WITH_ASSIGNEES); + const lowSeverityFeatureFlawIssue: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION_LOW_SEVERITY); + const highSeverityDocumentationBugIssue: Issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION_HIGH_SEVERITY); const issuesList: Issue[] = [ mediumSeverityIssueWithResponse, @@ -26,26 +26,26 @@ describe('search-filter', () => { highSeverityDocumentationBugIssue ]; const displayedColumns: string[] = [TABLE_COLUMNS.ID, TABLE_COLUMNS.TITLE, TABLE_COLUMNS.ASSIGNEE, TABLE_COLUMNS.LABEL]; - const issueService: IssueService = new IssueService(null, null, null); + const repoItemService: RepoItemService = new RepoItemService(null, null, null); it('can filter for issues which are assigned to a specific user', () => { searchKey = USER_ANUBHAV.loginId; - expect(applySearchFilter(searchKey, displayedColumns, issueService, issuesList)).toEqual([mediumSeverityIssueWithAssigneee]); + expect(applySearchFilter(searchKey, displayedColumns, repoItemService, issuesList)).toEqual([mediumSeverityIssueWithAssigneee]); }); it('can filter for issues using label', () => { searchKey = GITHUB_LABEL_FEATURE_FLAW.name; - expect(applySearchFilter(searchKey, displayedColumns, issueService, issuesList)).toEqual([lowSeverityFeatureFlawIssue]); + expect(applySearchFilter(searchKey, displayedColumns, repoItemService, issuesList)).toEqual([lowSeverityFeatureFlawIssue]); }); it('can filter for issues that contain the search key in any other column', () => { // Search by id of issue searchKey = mediumSeverityIssueWithResponse.id.toString(); - expect(applySearchFilter(searchKey, displayedColumns, issueService, issuesList)).toEqual([mediumSeverityIssueWithResponse]); + expect(applySearchFilter(searchKey, displayedColumns, repoItemService, issuesList)).toEqual([mediumSeverityIssueWithResponse]); // Search by title of issue searchKey = mediumSeverityIssueWithAssigneee.title; - expect(applySearchFilter(searchKey, displayedColumns, issueService, issuesList)).toEqual([mediumSeverityIssueWithAssigneee]); + expect(applySearchFilter(searchKey, displayedColumns, repoItemService, issuesList)).toEqual([mediumSeverityIssueWithAssigneee]); }); }); }); diff --git a/tests/constants/session.constants.ts b/tests/constants/session.constants.ts index 55147bae3..7769f81bc 100644 --- a/tests/constants/session.constants.ts +++ b/tests/constants/session.constants.ts @@ -6,7 +6,7 @@ export const WATCHER_REPO: Repo = Repo.of('CATcher-org/WATcher'); export const CATCHER_REPO: Repo = Repo.of('CATcher-org/CATcher'); const ISSUES_VIEWER_SESSION_REPO: SessionRepo = { - view: View.issuesViewer, + view: View.repoItemsViewer, repos: [WATCHER_REPO] }; diff --git a/tests/model/issue.model.spec.ts b/tests/model/issue.model.spec.ts index 092b6b7c5..5859e197b 100644 --- a/tests/model/issue.model.spec.ts +++ b/tests/model/issue.model.spec.ts @@ -1,6 +1,7 @@ import * as moment from 'moment'; import { Issue } from '../../src/app/core/models/issue.model'; import { Milestone } from '../../src/app/core/models/milestone.model'; +import { RepoItem } from '../../src/app/core/models/repo-item.model'; import { USER_ANUBHAV } from '../constants/data.constants'; import { CLOSED_ISSUE_WITH_EMPTY_DESCRIPTION, @@ -19,7 +20,7 @@ import { describe('Issue model class', () => { describe('.createPhaseBugReportIssue(githubIssue)', () => { it('should correctly create a issue that has an empty description', async () => { - const issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION); + const issue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION); expect(issue.globalId).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.id); expect(issue.id).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.number); @@ -31,7 +32,7 @@ describe('Issue model class', () => { expect(issue.milestone).toEqual(new Milestone(MILESTONE_ONE)); expect(issue.state).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.state); expect(issue.stateReason).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.stateReason); - expect(issue.issueOrPr).toEqual('Issue'); + expect(issue.type === 'Issue').toEqual(true); expect(issue.author).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.user.login); expect(issue.isDraft).toEqual(ISSUE_WITH_EMPTY_DESCRIPTION.isDraft); expect(issue.assignees).toEqual([]); @@ -45,19 +46,19 @@ describe('Issue model class', () => { }); it('should set close date correctly for closed issue', () => { - const issue = Issue.createPhaseBugReportingIssue(CLOSED_ISSUE_WITH_EMPTY_DESCRIPTION); + const issue = Issue.createIssue(CLOSED_ISSUE_WITH_EMPTY_DESCRIPTION); expect(issue.closed_at).toEqual(moment(CLOSED_ISSUE_WITH_EMPTY_DESCRIPTION.closed_at).format('lll')); }); it('should set milestone to default milestone for issue without milestone', () => { - const issue = Issue.createPhaseBugReportingIssue(ISSUE_WITHOUT_MILESTONE); + const issue = Issue.createIssue(ISSUE_WITHOUT_MILESTONE); expect(issue.milestone).toEqual(Milestone.IssueWithoutMilestone); }); it('should set assignees correctly for issue with assignees', () => { - const issue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_ASSIGNEES); + const issue = Issue.createIssue(ISSUE_WITH_ASSIGNEES); expect(issue.assignees).toEqual([USER_ANUBHAV.loginId]); }); @@ -66,21 +67,21 @@ describe('Issue model class', () => { describe('.updateDescription(description)', () => { it('correctly clean strings obtained from users', () => { const noDetailsFromBugReporter = 'No details provided by bug reporter.'; - expect(Issue.updateDescription('')).toBe(noDetailsFromBugReporter); - expect(Issue.updateDescription(null)).toBe(noDetailsFromBugReporter); + expect(RepoItem.updateDescription('')).toBe(noDetailsFromBugReporter); + expect(RepoItem.updateDescription(null)).toBe(noDetailsFromBugReporter); const typicalDescription = 'The app crashes after parsing config files.'; - expect(Issue.updateDescription(typicalDescription)).toBe(typicalDescription + '\n\n'); + expect(RepoItem.updateDescription(typicalDescription)).toBe(typicalDescription + '\n\n'); const inputWithSpecialChars = '$%^!@&-_test'; - expect(Issue.updateDescription(inputWithSpecialChars)).toBe(inputWithSpecialChars + '\n\n'); + expect(RepoItem.updateDescription(inputWithSpecialChars)).toBe(inputWithSpecialChars + '\n\n'); }); }); }); describe('Issue', () => { - const dummyIssue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_EMPTY_DESCRIPTION); - const otherDummyIssue = Issue.createPhaseBugReportingIssue(ISSUE_WITH_ASSIGNEES); + const dummyIssue = Issue.createIssue(ISSUE_WITH_EMPTY_DESCRIPTION); + const otherDummyIssue = Issue.createIssue(ISSUE_WITH_ASSIGNEES); const noReportedDescriptionString = 'No details provided by bug reporter.\n'; diff --git a/tests/services/githubevent.service.spec.ts b/tests/services/githubevent.service.spec.ts index 19cefe982..58bcb7963 100644 --- a/tests/services/githubevent.service.spec.ts +++ b/tests/services/githubevent.service.spec.ts @@ -3,19 +3,19 @@ import { GithubEventService } from '../../src/app/core/services/githubevent.serv import { ADD_LABEL_EVENT, CHANGE_TITLE_EVENT, EVENTS } from '../constants/githubevent.constants'; let githubService: any; -let issueService: any; +let repoItemService: any; describe('GithubEventService', () => { beforeAll(() => { githubService = jasmine.createSpyObj('GithubService', ['fetchEventsForRepo']); - issueService = jasmine.createSpyObj('IssueService', ['reloadAllIssues']); - issueService.reloadAllIssues.and.returnValue(of([])); + repoItemService = jasmine.createSpyObj('RepoItemService', ['reloadAllRepoItems']); + repoItemService.reloadAllRepoItems.and.returnValue(of([])); }); describe('.setLatestChangeEvent()', () => { it('stores the time of the most recent issue event and most recent issue update.', async () => { githubService.fetchEventsForRepo.and.returnValue(of(EVENTS)); - const githubEventService: GithubEventService = new GithubEventService(githubService, issueService); + const githubEventService: GithubEventService = new GithubEventService(githubService, repoItemService); await githubEventService.setLatestChangeEvent().toPromise(); githubEventService.reloadPage().subscribe((result) => expect(result).toBe(false)); }); @@ -23,26 +23,26 @@ describe('GithubEventService', () => { describe('.reloadPage()', () => { afterEach(() => { - issueService.reloadAllIssues.calls.reset(); + repoItemService.reloadAllRepoItems.calls.reset(); }); - it('triggers the IssueService to re-initialise the issue list if there are new events', async () => { + it('triggers the RepoItemService to re-initialise the repo item list if there are new events', async () => { const FIRST_EVENT = [ADD_LABEL_EVENT]; const SECOND_EVENT = [CHANGE_TITLE_EVENT]; githubService.fetchEventsForRepo.and.returnValue(of(FIRST_EVENT)); - const githubEventService: GithubEventService = new GithubEventService(githubService, issueService); + const githubEventService: GithubEventService = new GithubEventService(githubService, repoItemService); githubEventService.reloadPage().subscribe((result) => expect(result).toBe(true)); githubService.fetchEventsForRepo.and.returnValue(of(SECOND_EVENT)); githubEventService.reloadPage().subscribe((result) => expect(result).toBe(true)); }); - it('does not trigger the IssueService to re-initialise the issue list, if there are no new events', () => { + it('does not trigger the RepoItemService to re-initialise the issue list, if there are no new events', () => { githubService.fetchEventsForRepo.and.returnValue(of(EVENTS)); - const githubEventService: GithubEventService = new GithubEventService(githubService, issueService); + const githubEventService: GithubEventService = new GithubEventService(githubService, repoItemService); githubEventService.reloadPage().subscribe((result) => expect(result).toBe(true)); - // issueService.reloadAllIssues must not have been called again + // repoItemService.reloadAllRepoItems must not have been called again githubEventService.reloadPage().subscribe((result) => expect(result).toBe(false)); }); }); @@ -50,7 +50,7 @@ describe('GithubEventService', () => { describe('.reset()', () => { it('clears the details of the most recent event', async () => { githubService.fetchEventsForRepo.and.returnValue(of(EVENTS)); - const githubEventService: GithubEventService = new GithubEventService(githubService, issueService); + const githubEventService: GithubEventService = new GithubEventService(githubService, repoItemService); await githubEventService.setLatestChangeEvent().toPromise(); githubEventService.reset(); diff --git a/tests/services/view.service.spec.ts b/tests/services/view.service.spec.ts index 5bd547770..7fca4b1cc 100644 --- a/tests/services/view.service.spec.ts +++ b/tests/services/view.service.spec.ts @@ -57,7 +57,7 @@ describe('ViewService', () => { it('should navigate to the new repository', () => { viewService.setRepository(WATCHER_REPO); - expect(routerSpy.navigate).toHaveBeenCalledWith(['issuesViewer'], { + expect(routerSpy.navigate).toHaveBeenCalledWith(['repoItemsViewer'], { queryParams: { repo: WATCHER_REPO.toString() }, queryParamsHandling: 'merge' }); @@ -99,7 +99,7 @@ describe('ViewService', () => { expect(loggingServiceSpy.info).toHaveBeenCalledWith(`ViewService: Changing current repository to '${WATCHER_REPO}'`); expect(viewService.currentRepo).toEqual(WATCHER_REPO); - expect(routerSpy.navigate).toHaveBeenCalledWith(['issuesViewer'], { + expect(routerSpy.navigate).toHaveBeenCalledWith(['repoItemsViewer'], { queryParams: { repo: WATCHER_REPO.toString() }, queryParamsHandling: 'merge' }); @@ -128,7 +128,7 @@ describe('ViewService', () => { expect(loggingServiceSpy.info).toHaveBeenCalledWith(`ViewService: Repo is ${WATCHER_REPO}`); expect(viewService.currentRepo).toEqual(WATCHER_REPO); - expect(routerSpy.navigate).toHaveBeenCalledWith(['issuesViewer'], { + expect(routerSpy.navigate).toHaveBeenCalledWith(['repoItemsViewer'], { queryParams: { repo: WATCHER_REPO.toString() }, queryParamsHandling: 'merge' }); @@ -147,7 +147,7 @@ describe('ViewService', () => { it('should set current view', () => { viewService.setRepository(WATCHER_REPO); - expect(viewService.currentView).toEqual(View.issuesViewer); + expect(viewService.currentView).toEqual(View.repoItemsViewer); viewService.changeView(View.activityDashboard); @@ -159,13 +159,13 @@ describe('ViewService', () => { it('should reset the currentView of the ViewService', () => { viewService.currentView = View.activityDashboard; viewService.reset(); - expect(viewService.currentView).toBe(View.issuesViewer); + expect(viewService.currentView).toBe(View.repoItemsViewer); }); }); describe('setupFromUrl(url)', () => { it('should set items in local storage if url is valid', async () => { - const validUrl = `/issuesViewer?repo=${WATCHER_REPO.owner}%2F${WATCHER_REPO.name}`; + const validUrl = `/repoItemsViewer?repo=${WATCHER_REPO.owner}%2F${WATCHER_REPO.name}`; const localStorageSetItemSpy = spyOn(window.localStorage, 'setItem'); await viewService.setupFromUrl(validUrl).toPromise(); @@ -175,7 +175,7 @@ describe('ViewService', () => { }); it('should throw error for url without repo paramater', (done) => { - const urlWithoutRepo = '/issuesViewer'; + const urlWithoutRepo = '/repoItemsViewer'; viewService.setupFromUrl(urlWithoutRepo).subscribe({ error: (err) => { @@ -197,7 +197,7 @@ describe('ViewService', () => { }); it('should throw error for url with invalid repo format', (done) => { - const urlWithInvalidRepoFormat = '/issuesViewer?repo=InvalidRepo'; + const urlWithInvalidRepoFormat = '/repoItemsViewer?repo=InvalidRepo'; viewService.setupFromUrl(urlWithInvalidRepoFormat).subscribe({ error: (err) => {