Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for issues closed as not planned #191

Merged
merged 13 commits into from
Nov 3, 2023
1 change: 1 addition & 0 deletions graphql/fragments/issue.fragment.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ fragment issue on Issue {
title
body
state
stateReason
createdAt
updatedAt
url
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@graphql-codegen/typescript-document-nodes": "1.17.7",
"@graphql-codegen/typescript-operations": "^1.18.4",
"@graphql-codegen/typescript-resolvers": "^1.20.0",
"@octokit/graphql-schema": "^8.24.0",
"@octokit/graphql-schema": "^10.74.0",
"@types/jasmine": "^3.8.2",
"@types/jasminewd2": "2.0.8",
"@types/node": "^14.17.6",
Expand Down
13 changes: 12 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IssueServiceFactory } from './core/services/factories/factory.issue.ser
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 { PhaseService } from './core/services/phase.service';
import { SessionFixConfirmationComponent } from './core/services/session-fix-confirmation/session-fix-confirmation.component';
Expand Down Expand Up @@ -64,7 +65,17 @@ import { SharedModule } from './shared/shared.module';
{
provide: AuthService,
useFactory: AuthServiceFactory,
deps: [Router, NgZone, GithubService, UserService, IssueService, PhaseService, GithubEventService, Title, LoggingService]
deps: [
Router,
NgZone,
GithubService,
UserService,
IssueService,
LabelService,
PhaseService,
GithubEventService,
Title,
LoggingService]
},
{
provide: IssueService,
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/github/github-graphql.issue-or-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class GithubGraphqlIssueOrPr extends GithubIssue {
url: String(issue.url),
title: issue.title,
state: issue.state,
stateReason: issue.stateReason,
user: {
login: issue.author.login
},
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/github/github-graphql.issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class GithubGraphqlIssue extends GithubIssue {
url: String(issue.url),
title: issue.title,
state: issue.state,
stateReason: issue.stateReason,
user: {
login: issue.author.login
},
Expand Down
12 changes: 10 additions & 2 deletions src/app/core/models/github/github-issue-filter.model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { IssueFilters, IssueState } from '../../../../../graphql/graphql-types';
import { IssueFilters, IssueState, IssueStateReason } from '../../../../../graphql/graphql-types';

export type RestGithubIssueState = 'open' | 'close' | 'all';
export type RestGithubIssueStateReason = 'completed' | 'notPlanned' | 'reopened';
export type RestGithubSortBy = 'created' | 'updated' | 'comments';
export type RestGithubSortDir = 'asc' | 'desc';

export interface RestGithubIssueFilterData {
state?: RestGithubIssueState;
stateReason?: RestGithubIssueStateReason;
labels?: Array<string>;
sort?: RestGithubSortBy;
direction?: RestGithubSortDir;
Expand All @@ -21,6 +23,7 @@ export interface RestGithubIssueFilterData {
* */
export default class RestGithubIssueFilter implements RestGithubIssueFilterData {
state?: RestGithubIssueState;
stateReason?: RestGithubIssueStateReason;
labels?: Array<string>;
sort?: RestGithubSortBy;
direction?: RestGithubSortDir;
Expand Down Expand Up @@ -53,7 +56,12 @@ export default class RestGithubIssueFilter implements RestGithubIssueFilterData
mentioned: this.mentioned,
milestone: this.milestone,
since: this.since,
states: [this.state === 'close' ? IssueState.Closed : IssueState.Open]
states: [this.state === 'close' ? IssueState.Closed : IssueState.Open],
stateReason: [this.stateReason === 'completed'
? IssueStateReason.Completed
: this.stateReason === 'notPlanned'
? IssueStateReason.NotPlanned
: IssueStateReason.Reopened]
};
}
}
3 changes: 2 additions & 1 deletion src/app/core/models/github/github-issue.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IssueState } from '../../../../../graphql/graphql-types';
import { IssueState, IssueStateReason } from '../../../../../graphql/graphql-types';
import { GithubComment } from './github-comment.model';
import { GithubLabel } from './github-label.model';

Expand All @@ -12,6 +12,7 @@ export class GithubIssue {
created_at: string;
labels: Array<GithubLabel>;
state: IssueState;
stateReason: IssueStateReason;
title: string;
updated_at: string;
closed_at: string;
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/models/issue.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class Issue {
closed_at: string;
milestone: Milestone;
state: string;
stateReason: string;
issueOrPr: string;
author: string;

Expand Down Expand Up @@ -83,6 +84,7 @@ export class Issue {
// githubIssue without milestone will be set to default milestone
this.milestone = githubIssue.milestone ? new Milestone(githubIssue.milestone) : Milestone.DefaultMilestone;
this.state = githubIssue.state;
this.stateReason = githubIssue.stateReason;
this.issueOrPr = githubIssue.issueOrPr;
this.author = githubIssue.user.login;
// this.githubIssue = githubIssue;
Expand Down
21 changes: 11 additions & 10 deletions src/app/core/models/label.model.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
/**
* Represents a label and its attributes.
*/
export class Label {
export class Label implements SimpleLabel {
readonly category: string;
readonly name: string;
readonly formattedName: string; // 'category'.'name' (e.g. severity.Low) if a category exists or 'name' if the category does not exist.
color: string;
definition?: string;

constructor(label: { name: string; color: string; definition?: string }) {
const containsDotRegex = /\.\b/g; // contains dot in middle of name
[this.category, this.name] = containsDotRegex.test(label.name) ? label.name.split('.') : [undefined, label.name];
this.formattedName = this.category === undefined || this.category === '' ? this.name : this.category.concat('.', this.name);
this.color = label.color;
this.definition = label.definition;
}

/**
* Returns the name of the label with the format of
* 'category'.'name' (e.g. severity.Low) if a category exists or
* 'name' if the category does not exist.
*/
public getFormattedName(): string {
return this.category === undefined || this.category === '' ? this.name : this.category.concat('.', this.name);
}

public equals(label: Label) {
return this.name === label.name && this.category === label.category;
}
}

/**
* Represents a simplified label with name and color
*/
export type SimpleLabel = {
formattedName: string;
color: string;
};
5 changes: 4 additions & 1 deletion src/app/core/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { uuid } from '../../shared/lib/uuid';
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 { PhaseService } from './phase.service';
import { UserService } from './user.service';
Expand Down Expand Up @@ -42,6 +43,7 @@ export class AuthService {
private githubService: GithubService,
private userService: UserService,
private issueService: IssueService,
private labelService: LabelService,
private phaseService: PhaseService,
private githubEventService: GithubEventService,
private titleService: Title,
Expand All @@ -67,6 +69,7 @@ export class AuthService {
this.githubService.reset();
this.userService.reset();
this.issueService.reset(true);
this.labelService.reset();
this.phaseService.reset();
this.githubEventService.reset();
this.logger.reset();
Expand Down Expand Up @@ -121,7 +124,7 @@ export class AuthService {
startOAuthProcess() {
this.logger.info('AuthService: Starting authentication');
// Available OAuth scopes https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
const githubRepoPermission = 'public_repo';
const githubRepoPermission = 'repo';
this.changeAuthState(AuthState.AwaitingAuthentication);

this.generateStateString();
Expand Down
15 changes: 14 additions & 1 deletion src/app/core/services/factories/factory.auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthService } from '../auth.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 { MockAuthService } from '../mocks/mock.auth.service';
import { PhaseService } from '../phase.service';
Expand All @@ -17,6 +18,7 @@ export function AuthServiceFactory(
githubService: GithubService,
userService: UserService,
issueService: IssueService,
labelService: LabelService,
phaseService: PhaseService,
githubEventService: GithubEventService,
titleService: Title,
Expand All @@ -30,12 +32,23 @@ export function AuthServiceFactory(
// githubService,
// userService,
// issueService,
// labelService,
// phaseService,
// githubEventService,
// titleService,
// logger
// );
// }
console.log(logger);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
console.log(logger);

not part of this PR but let's remove the debugging statement.

return new AuthService(router, ngZone, githubService, userService, issueService, phaseService, githubEventService, titleService, logger);
return new AuthService(
router,
ngZone,
githubService,
userService,
issueService,
labelService,
phaseService,
githubEventService,
titleService,
logger);
}
69 changes: 47 additions & 22 deletions src/app/core/services/github.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { Apollo, QueryRef } from 'apollo-angular';
import { ApolloQueryResult } from 'apollo-client';
import { DocumentNode } from 'graphql';
import { forkJoin, from, Observable, of, throwError, zip } from 'rxjs';
import { BehaviorSubject, forkJoin, from, merge, Observable, of, throwError } from 'rxjs';
import { catchError, filter, flatMap, map, throwIfEmpty } from 'rxjs/operators';
import {
FetchIssue,
Expand Down Expand Up @@ -62,7 +62,7 @@ export class GithubService {

private issuesCacheManager = new IssuesCacheManager();
private issuesLastModifiedManager = new IssueLastModifiedManagerModel();
private issueQueryRefs = new Map<Number, QueryRef<FetchIssueQuery>>();
private issueQueryRefs = new Map<number, QueryRef<FetchIssueQuery>>();

constructor(private errorHandlingService: ErrorHandlingService, private apollo: Apollo, private logger: LoggingService) {}

Expand Down Expand Up @@ -138,7 +138,7 @@ export class GithubService {
);

// Concatenate both streams together.
return zip(issueObs, prObs).pipe(map((x) => x[0].concat(x[1])));
return merge(issueObs, prObs);
}

/**
Expand Down Expand Up @@ -483,8 +483,8 @@ export class GithubService {
pluckEdges: (results: ApolloQueryResult<T>) => Array<any>,
Model: new (data) => M
): Observable<Array<M>> {
return from(this.withPagination<T>(pluckEdges)(query, variables)).pipe(
map((results: Array<ApolloQueryResult<T>>) => {
return this.withPagination<T>(pluckEdges, query, variables, false).pipe(
map((results: ApolloQueryResult<T>[]) => {
const issues = results.reduce((accumulated, current) => accumulated.concat(pluckEdges(current)), []);
return issues.map((issue) => new Model(issue.node));
}),
Expand All @@ -495,32 +495,57 @@ export class GithubService {
}

/**
* Returns an async function that will accept a GraphQL query that requests for paginated items.
* Said function will recursively query for all subsequent pages until a page that has less than 100 items is found,
* then return all queried pages in an array.
* Returns an observable that will continually emit the currently accumulated results, until a page that has less
* than 100 items is found, after which it performs a final emit with the full results array, and completes.
*
* If `shouldAccumulate` is false, the observable will emit only the latest result, it will still complete on the
* same condition.
*
* @callback pluckEdges - A function that returns a list of edges in a ApolloQueryResult.
* @returns an async function that accepts a GraphQL query for paginated data and any additional variables to that query
* @params query - The query to be performed.
* @params variables - The variables for the query.
* @params shouldAccumulate - Whether the observable should accumulate the results.
* @returns an observable
*/
private withPagination<T>(pluckEdges: (results: ApolloQueryResult<T>) => Array<any>) {
return async (query: DocumentNode, variables: { [key: string]: any } = {}): Promise<Array<ApolloQueryResult<T>>> => {
const maxResultsCount = 100;
const cursor = variables.cursor || null;
const graphqlQuery = this.apollo.watchQuery<T>({ query, variables: { ...variables, cursor } });
return graphqlQuery.refetch().then(async (results: ApolloQueryResult<T>) => {
private withPagination<T>(
pluckEdges: (results: ApolloQueryResult<T>) => Array<any>,
query: DocumentNode,
variables: { [key: string]: any } = {},
shouldAccumulate: boolean = true
): Observable<ApolloQueryResult<T>[]> {
const maxResultsCount = 100;
const apollo = this.apollo;

let accumulatedResults: ApolloQueryResult<T>[] = [];
const behaviorSubject: BehaviorSubject<ApolloQueryResult<T>[]> = new BehaviorSubject(accumulatedResults);

async function queryWith(cursor: string): Promise<void> {
const graphqlQuery = apollo.watchQuery<T>({ query, variables: { ...variables, cursor } });

await graphqlQuery.refetch().then(async (results: ApolloQueryResult<T>) => {
const intermediate = Array.isArray(results) ? results : [results];
const edges = pluckEdges(results);
const nextCursor = edges.length === 0 ? null : edges[edges.length - 1].cursor;

if (shouldAccumulate) {
accumulatedResults = accumulatedResults.concat(intermediate);
behaviorSubject.next(accumulatedResults);
} else {
behaviorSubject.next(intermediate);
}
if (edges.length < maxResultsCount || !nextCursor) {
return intermediate;
// No more queries to perform.
behaviorSubject.complete();
return;
}
const nextResults = await this.withPagination<T>(pluckEdges)(query, {
...variables,
cursor: nextCursor
});
return intermediate.concat(nextResults);

// Use a chain of await to ensure that all recursive queries are completed before `complete` is called.
await queryWith(nextCursor);
});
};
}

queryWith(null);

return behaviorSubject.asObservable();
}
}
Loading
Loading