diff --git a/angular.json b/angular.json index 933765bc6..a2a687548 100644 --- a/angular.json +++ b/angular.json @@ -25,18 +25,8 @@ "src/favicon.256x256.png", "src/favicon.512x512.png" ], - "styles": [ - "node_modules/prismjs/themes/prism-okaidia.css", - "src/styles.css", - "src/markdown.css", - "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" - ], - "scripts": [ - "node_modules/marked/lib/marked.js", - "node_modules/prismjs/prism.js", - "node_modules/prismjs/components/prism-csharp.min.js", - "node_modules/prismjs/components/prism-css.min.js" - ] + "styles": ["src/styles.css", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"], + "scripts": [] }, "configurations": { "staging": { diff --git a/package.json b/package.json index 1da91598f..71edb5717 100644 --- a/package.json +++ b/package.json @@ -41,26 +41,18 @@ "@octokit/rest": "^16.37.0", "@primer/octicons": "^17.12.0", "@types/geojson": "7946.0.8", - "ajv": "^6.11.0", "apollo-angular": "^1.9.1", "apollo-angular-link-http": "^1.10.0", "apollo-cache-inmemory": "^1.6.0", "apollo-client": "^2.6.0", "apollo-link": "^1.2.14", "apollo-link-context": "^1.0.20", - "arcsecond": "^4.1.0", "core-js": "^3.28.0", - "d3": "^7.4.4", - "d3-time": "^3.0.0", - "d3-time-format": "^4.1.0", - "d3-timelines": "^1.3.1", "diff-match-patch": "^1.0.4", - "dompurify": "^2.4.4", "graphql": "^14.6.0", "graphql-tag": "2.11.0", "karma-spec-reporter": "0.0.32", "moment": "^2.24.0", - "ngx-markdown": "^10.1.1", "ngx-mat-select-search": "^3.3.3", "node-fetch": "^2.6.9", "rxjs": "6.5.5", @@ -79,8 +71,6 @@ "@graphql-codegen/typescript-operations": "^1.18.4", "@graphql-codegen/typescript-resolvers": "^1.20.0", "@octokit/graphql-schema": "^8.24.0", - "@types/d3": "^7.4.0", - "@types/dompurify": "^2.3.1", "@types/jasmine": "^3.8.2", "@types/jasminewd2": "2.0.8", "@types/node": "^14.17.6", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 153817fa7..967103563 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -8,7 +8,6 @@ import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http'; import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import { ApolloLink } from 'apollo-link'; import { setContext } from 'apollo-link-context'; -import { MarkdownModule, MarkedOptions } from 'ngx-markdown'; import 'reflect-metadata'; import graphqlTypes from '../../graphql/graphql-types'; import '../polyfills'; @@ -33,7 +32,6 @@ import { UserService } from './core/services/user.service'; import { IssuesViewerModule } from './issues-viewer/issues-viewer.module'; import { LabelDefinitionPopupComponent } from './shared/label-definition-popup/label-definition-popup.component'; import { HeaderComponent } from './shared/layout'; -import { markedOptionsFactory } from './shared/lib/marked'; import { RepoChangeFormComponent } from './shared/repo-change-form/repo-change-form.component'; import { SharedModule } from './shared/shared.module'; @@ -54,12 +52,6 @@ import { SharedModule } from './shared/shared.module'; ActivityDashboardModule, SharedModule, HttpClientModule, - MarkdownModule.forRoot({ - markedOptions: { - provide: MarkedOptions, - useFactory: markedOptionsFactory - } - }), AppRoutingModule, ApolloModule, HttpLinkModule diff --git a/src/app/auth/session-selection/session-selection.component.html b/src/app/auth/session-selection/session-selection.component.html index f3e94f897..e16fb48a3 100644 --- a/src/app/auth/session-selection/session-selection.component.html +++ b/src/app/auth/session-selection/session-selection.component.html @@ -6,15 +6,10 @@
diff --git a/src/app/auth/session-selection/session-selection.component.ts b/src/app/auth/session-selection/session-selection.component.ts index 790f32349..3a71b5331 100644 --- a/src/app/auth/session-selection/session-selection.component.ts +++ b/src/app/auth/session-selection/session-selection.component.ts @@ -1,11 +1,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; import { Profile } from '../../core/models/profile.model'; import { AuthService, AuthState } from '../../core/services/auth.service'; import { ErrorHandlingService } from '../../core/services/error-handling.service'; import { LoggingService } from '../../core/services/logging.service'; +import { RepoUrlCacheService } from '../../core/services/repo-url-cache.service'; @Component({ selector: 'app-session-selection', @@ -17,7 +17,6 @@ export class SessionSelectionComponent implements OnInit { isSettingUpSession: boolean; profileForm: FormGroup; repoForm: FormGroup; - suggestions: string[]; filteredSuggestions: Observable; @Input() urlEncodedSessionName: string; @@ -29,6 +28,7 @@ export class SessionSelectionComponent implements OnInit { private formBuilder: FormBuilder, private logger: LoggingService, private authService: AuthService, + private repoUrlCacheService: RepoUrlCacheService, private errorHandlingService: ErrorHandlingService ) {} @@ -73,11 +73,7 @@ export class SessionSelectionComponent implements OnInit { window.localStorage.setItem('org', repoOrg); window.localStorage.setItem('dataRepo', repoName); - // Update autofill repository URL suggestions in localStorage - if (!this.suggestions.includes(repoInformation)) { - this.suggestions.push(repoInformation); - window.localStorage.setItem('suggestions', JSON.stringify(this.suggestions)); - } + this.repoUrlCacheService.cache(repoInformation); } this.logger.info(`SessionSelectionComponent: Selected Repository: ${repoInformation}`); @@ -117,13 +113,8 @@ export class SessionSelectionComponent implements OnInit { this.repoForm = this.formBuilder.group({ repo: ['', Validators.required] }); - this.suggestions = JSON.parse(window.localStorage.getItem('suggestions')) || []; - // Ref: https://v10.material.angular.io/components/autocomplete/overview - this.filteredSuggestions = this.repoForm.get('repo').valueChanges - .pipe( - startWith(''), - map(value => this.suggestions.filter(suggestion => suggestion.toLowerCase().includes(value.toLowerCase()))) - ); + + this.filteredSuggestions = this.repoUrlCacheService.getFilteredSuggestions(this.repoForm.get('repo')); } private autofillRepo() { diff --git a/src/app/core/models/assignee.model.ts b/src/app/core/models/assignee.model.ts deleted file mode 100644 index 21def1e16..000000000 --- a/src/app/core/models/assignee.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface UserData { - id: number; - login: string; - url: string; -} - -export default class Assignee implements UserData { - id: number; - login: string; - url: string; - - constructor(data: UserData) { - Object.assign(this, data); - this.login = data.login.toLowerCase(); - } -} diff --git a/src/app/core/models/checkbox.model.ts b/src/app/core/models/checkbox.model.ts deleted file mode 100644 index 17a7d6de0..000000000 --- a/src/app/core/models/checkbox.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class Checkbox { - description: string; // in the format of - [ ] or - [x] - isChecked: boolean; - - constructor(description: string, isChecked: boolean) { - this.description = description; - this.isChecked = isChecked; - } - - setChecked(isChecked: boolean) { - this.isChecked = isChecked; - } - - toString(): string { - return `- ${this.isChecked ? '[x]' : '[ ]'} ${this.description}`; - } -} diff --git a/src/app/core/models/conflict/addition.model.ts b/src/app/core/models/conflict/addition.model.ts deleted file mode 100644 index c569f3039..000000000 --- a/src/app/core/models/conflict/addition.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Changes } from './changes.model'; - -export class Addition extends Changes { - readonly TYPE = 'ADDITION'; - readonly TAG = 'ins'; - readonly STYLES = ['background: #d4fcbc', 'text-decoration: none']; - readonly content: string; - - constructor(content: string) { - super(); - this.content = content; - } -} diff --git a/src/app/core/models/conflict/changes.model.ts b/src/app/core/models/conflict/changes.model.ts deleted file mode 100644 index 0bdc74a2b..000000000 --- a/src/app/core/models/conflict/changes.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { escapeHTML } from '../../../shared/lib/html'; - -export abstract class Changes { - abstract readonly TYPE: string; - abstract readonly TAG: string; - abstract readonly STYLES: string[]; - abstract readonly content: string; - - getHtmlString(): string { - return `<${this.TAG} style="${this.STYLES.join(';')}">${escapeHTML(this.content)}`; - } -} diff --git a/src/app/core/models/conflict/conflict.model.ts b/src/app/core/models/conflict/conflict.model.ts deleted file mode 100644 index 29d96cd62..000000000 --- a/src/app/core/models/conflict/conflict.model.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { diff_match_patch } from 'diff-match-patch'; -import { escapeHTML, replaceNewlinesWithBreakLines } from '../../../shared/lib/html'; -import { Addition } from './addition.model'; -import { Changes } from './changes.model'; -import { NoChange } from './no-change.model'; -import { Removal } from './removal.model'; - -/** - * A model to represent the difference/conflict between two text. - */ -export class Conflict { - outdatedContent: string; - updatedContent: string; - changes: Changes[] = []; - - constructor(outdatedContent: string, updatedContent: string) { - this.outdatedContent = outdatedContent; - this.updatedContent = updatedContent; - - const matcher = new diff_match_patch(); - const diffs = matcher.diff_main(outdatedContent, updatedContent); - matcher.diff_cleanupSemantic(diffs); - for (const diff of diffs) { - if (diff[0] === -1) { - this.changes.push(new Removal(diff[1])); - } else if (diff[0] === 1) { - this.changes.push(new Addition(diff[1])); - } else { - this.changes.push(new NoChange(diff[1])); - } - } - } - - getHtmlDiffString(): string { - let result = ''; - for (const change of this.changes) { - result += change.getHtmlString(); - } - return replaceNewlinesWithBreakLines(result); - } - - getHtmlUpdatedString(): string { - return replaceNewlinesWithBreakLines(escapeHTML(this.updatedContent)); - } -} diff --git a/src/app/core/models/conflict/no-change.model.ts b/src/app/core/models/conflict/no-change.model.ts deleted file mode 100644 index ffb31578d..000000000 --- a/src/app/core/models/conflict/no-change.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Changes } from './changes.model'; - -export class NoChange extends Changes { - readonly TYPE = 'NO_CHANGE'; - readonly TAG = 'span'; - readonly STYLES = []; - readonly content: string; - - constructor(content: string) { - super(); - this.content = content; - } -} diff --git a/src/app/core/models/conflict/removal.model.ts b/src/app/core/models/conflict/removal.model.ts deleted file mode 100644 index 306829673..000000000 --- a/src/app/core/models/conflict/removal.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Changes } from './changes.model'; - -export class Removal extends Changes { - readonly TYPE = 'REMOVAL'; - readonly TAG = 'del'; - readonly STYLES = ['background: #fbb']; - readonly content: string; - - constructor(content: string) { - super(); - this.content = content; - } -} diff --git a/src/app/core/models/generators/github-issue.generator.ts b/src/app/core/models/generators/github-issue.generator.ts deleted file mode 100644 index 0dc7240bc..000000000 --- a/src/app/core/models/generators/github-issue.generator.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GithubIssue } from '../github/github-issue.model'; - -export default function generateGithubIssuesArray(numberOfElements: number = 1): Array { - const created_and_updated_date: string = getRandomDate().toISOString(); - return new Array(10).map((value: GithubIssue, index: number, array: GithubIssue[]) => { - return new GithubIssue({ - id: index, - number: Math.random(), - assignees: undefined, - body: `Automatically Generated Issue No id: ${index}.`, - created_at: created_and_updated_date, - labels: undefined, - title: `Autogen Issue ${index}`, - updated_at: created_and_updated_date, - url: '', - user: undefined, - comments: undefined - }); - }); -} - -/** - * Returns a random Date between the start and end dates. - * @param start - Date representing the start of the date range. Default: 1/1/2018 - * @param end - Date representing the end of the date range. Default: Current Date - */ -function getRandomDate(start: Date = new Date(2018, 1, 1), end: Date = new Date()): Date { - return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); -} diff --git a/src/app/core/models/github/github-issue.model.ts b/src/app/core/models/github/github-issue.model.ts index 6000f1491..4343988d9 100644 --- a/src/app/core/models/github/github-issue.model.ts +++ b/src/app/core/models/github/github-issue.model.ts @@ -68,8 +68,4 @@ export class GithubIssue { } } } - - findTeamId(): string { - return `${this.findLabel('team')}.${this.findLabel('tutorial')}`; - } } diff --git a/src/app/core/models/issue-dispute.model.ts b/src/app/core/models/issue-dispute.model.ts deleted file mode 100644 index c68bd0e2f..000000000 --- a/src/app/core/models/issue-dispute.model.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Checkbox } from './checkbox.model'; -export class IssueDispute { - readonly TODO_DESCRIPTION = 'Done'; - readonly INITIAL_RESPONSE = '[replace this with your explanation]'; - readonly TITLE_PREFIX = '## :question: '; - readonly LINE_BREAK = '-------------------\n'; - title: string; // e.g Issue severity - description: string; // e.g Team says: xxx\n Tester says: xxx. - tutorResponse: string; // e.g Not justified. I've changed it back. - todo: Checkbox; // e.g - [x] Done - - constructor(title: string, description: string) { - this.title = title; - this.description = description; - this.tutorResponse = this.INITIAL_RESPONSE; - this.todo = new Checkbox(this.TODO_DESCRIPTION, false); - } - - isDone(): boolean { - return this.todo.isChecked; - } - - /* - This method is used to format the tutor's response so that the app can upload it on Github. - Refer to format in https://github.com/CATcher-org/templates#app-collect-tutor-response - */ - toTutorResponseString(): string { - let toString = ''; - toString += this.TITLE_PREFIX + this.title + '\n\n'; - toString += this.todo.toString() + '\n\n'; - toString += this.tutorResponse + '\n\n'; - toString += this.LINE_BREAK; - return toString; - } - - compareTo(anotherResponse: IssueDispute): number { - if (this.isDone() === anotherResponse.isDone()) { - return this.tutorResponse.localeCompare(anotherResponse.tutorResponse); - } - return this.isDone() ? 1 : -1; - } - - toString(): string { - let toString = ''; - toString += this.TITLE_PREFIX + this.title + '\n\n'; - toString += this.description + '\n\n'; - toString += this.LINE_BREAK; - return toString; - } - - setTutorResponse(response: string) { - this.tutorResponse = response; - } - - setIsDone(isDone: boolean) { - this.todo.setChecked(isDone); - } -} diff --git a/src/app/core/models/issue.model.ts b/src/app/core/models/issue.model.ts index ac6aa402b..21347efbf 100644 --- a/src/app/core/models/issue.model.ts +++ b/src/app/core/models/issue.model.ts @@ -1,19 +1,9 @@ import * as moment from 'moment'; -import { Phase } from '../models/phase.model'; -import { IssueComment } from './comment.model'; 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 { IssueDispute } from './issue-dispute.model'; import { Milestone } from './milestone.model'; -import { Team } from './team.model'; -import { TeamAcceptedTemplate } from './templates/team-accepted-template.model'; -import { TeamResponseTemplate } from './templates/team-response-template.model'; -import { TesterResponseTemplate } from './templates/tester-response-template.model'; -import { TutorModerationIssueTemplate } from './templates/tutor-moderation-issue-template.model'; -import { TutorModerationTodoTemplate } from './templates/tutor-moderation-todo-template.model'; -import { TesterResponse } from './tester-response.model'; export class Issue { /** Basic Fields */ @@ -32,36 +22,11 @@ export class Issue { issueOrPr: string; author: string; - /** Fields derived from Labels */ - severity: string; - type: string; - responseTag?: string; - duplicated?: boolean; - status?: string; - pending?: string; - unsure?: boolean; - teamAssigned?: Team; - /** Depending on the phase, assignees attribute can be derived from Github's assignee feature OR from the Github's issue description */ assignees?: string[]; labels?: string[]; githubLabels?: GithubLabel[]; - /** Fields derived from parsing of Github's issue description */ - duplicateOf?: number; - teamResponse?: string; - testerResponses?: TesterResponse[]; - testerDisagree?: boolean; // whether tester agrees or disagree to teams reponse - issueComment?: IssueComment; // Issue comment is used for Tutor Response and Tester Response - issueDisputes?: IssueDispute[]; - teamChosenSeverity?: string; - teamChosenType?: string; - teamAccepted?: boolean; - - /** Fields for error messages during parsing of Github's issue description */ - teamResponseError: boolean; - testerResponseError: boolean; - /** * Formats the text to create space at the end of the user input to prevent any issues with * the markdown interpretation. @@ -94,14 +59,6 @@ export class Issue { return Issue.orDefaultString(Issue.formatText(description), defaultString); } - /** - * Processes and cleans a raw team response obtained from user input. - */ - static updateTeamResponse(teamResponse: string): string { - const defaultString = 'No details provided by team.'; - return Issue.orDefaultString(Issue.formatText(teamResponse), 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. @@ -133,196 +90,21 @@ export class Issue { this.assignees = githubIssue.assignees.map((assignee) => assignee.login); this.githubLabels = githubIssue.labels; this.labels = githubIssue.labels.map((label) => label.name); - - /** Fields derived from Labels */ - this.severity = githubIssue.findLabel(GithubLabel.LABELS.severity); - this.type = githubIssue.findLabel(GithubLabel.LABELS.type); - this.responseTag = githubIssue.findLabel(GithubLabel.LABELS.response); - this.duplicated = !!githubIssue.findLabel(GithubLabel.LABELS.duplicated, false); - this.status = githubIssue.findLabel(GithubLabel.LABELS.status); - this.pending = githubIssue.findLabel(GithubLabel.LABELS.pending); } public static createPhaseBugReportingIssue(githubIssue: GithubIssue): Issue { return new Issue(githubIssue); } - public static createPhaseTeamResponseIssue(githubIssue: GithubIssue, teamData: Team): Issue { - const issue = new Issue(githubIssue); - const template = new TeamResponseTemplate(githubIssue.comments); - - issue.githubComments = githubIssue.comments; - issue.teamAssigned = teamData; - issue.assignees = githubIssue.assignees.map((assignee) => assignee.login); - - issue.teamResponseError = template.parseFailure; - issue.issueComment = template.comment; - issue.teamResponse = template.teamResponse && Issue.updateTeamResponse(template.teamResponse.content); - issue.duplicateOf = template.duplicateOf && template.duplicateOf.issueNumber; - issue.duplicated = issue.duplicateOf !== undefined && issue.duplicateOf !== null; - - return issue; - } - - public static createPhaseTesterResponseIssue(githubIssue: GithubIssue): Issue { - const issue = new Issue(githubIssue); - const testerResponseTemplate = new TesterResponseTemplate(githubIssue.comments); - const teamAcceptedTemplate = new TeamAcceptedTemplate(githubIssue.comments); - - issue.githubComments = githubIssue.comments; - issue.testerResponseError = testerResponseTemplate.parseFailure && teamAcceptedTemplate.parseFailure; - issue.teamAccepted = teamAcceptedTemplate.teamAccepted; - issue.issueComment = testerResponseTemplate.comment; - issue.teamResponse = testerResponseTemplate.teamResponse && Issue.updateTeamResponse(testerResponseTemplate.teamResponse.content); - issue.testerResponses = testerResponseTemplate.testerResponse && testerResponseTemplate.testerResponse.testerResponses; - issue.testerDisagree = testerResponseTemplate.testerDisagree; - - issue.teamChosenSeverity = testerResponseTemplate.teamChosenSeverity || null; - issue.teamChosenType = testerResponseTemplate.teamChosenType || null; - - return issue; - } - - public static createPhaseModerationIssue(githubIssue: GithubIssue, teamData: Team): Issue { - const issue = new Issue(githubIssue); - const issueTemplate = new TutorModerationIssueTemplate(githubIssue); - const todoTemplate = new TutorModerationTodoTemplate(githubIssue.comments); - - issue.githubComments = githubIssue.comments; - issue.teamAssigned = teamData; - issue.description = issueTemplate.description.content; - issue.teamResponse = issueTemplate.teamResponse && Issue.updateTeamResponse(issueTemplate.teamResponse.content); - issue.issueDisputes = issueTemplate.dispute.disputes; - - if (todoTemplate.moderation && todoTemplate.comment) { - issue.issueDisputes = todoTemplate.moderation.disputesToResolve.map((dispute, i) => { - dispute.description = issueTemplate.dispute.disputes[i].description; - return dispute; - }); - issue.issueComment = todoTemplate.comment; - } - return issue; - } - - /** - * Creates a new copy of an exact same issue. - * This would come useful in the event when you want to update the issue but not the actual - * state of the application. - */ - clone(phase: Phase): Issue { - switch (phase) { - case Phase.issuesViewer: - return Issue.createPhaseBugReportingIssue(this.githubIssue); - default: - return Issue.createPhaseBugReportingIssue(this.githubIssue); - } - } - - /** - * Depending on the phase of the peer testing, each phase will have a response associated to them. - * This function will allow the current instance of issue to retain the state of response of the given `issue`. - * - * @param phase - The phase in which you want to retain your responses. - * @param issue - The issue which you want your current instance to retain from. - */ - retainResponses(phase: Phase, issue: Issue) { - this.issueComment = issue.issueComment; - this.githubComments = issue.githubComments; - switch (phase) { - case Phase.issuesViewer: - this.description = issue.description; - break; - default: - break; - } - } - - /** - * Updates the tester's responses and team response based on the given githubComment. - * @param githubComment - A version of githubComment to update the issue with. - */ - updateTesterResponse(githubComment: GithubComment): void { - const template = new TesterResponseTemplate([githubComment]); - this.issueComment = template.comment; - this.teamResponse = template.teamResponse && template.teamResponse.content; - this.testerResponses = template.testerResponse && template.testerResponse.testerResponses; - } - - /** - * Updates the tutor's resolution of the disputes with a new version of githubComment. - * @param githubComment - A version of githubComment to update the dispute with. - */ - updateDispute(githubComment: GithubComment): void { - const todoTemplate = new TutorModerationTodoTemplate([githubComment]); - this.issueComment = todoTemplate.comment; - this.issueDisputes = todoTemplate.moderation.disputesToResolve.map((dispute, i) => { - dispute.description = this.issueDisputes[i].description; - return dispute; - }); - } - createGithubIssueDescription(): string { return `${this.description}\n${this.hiddenDataInDescription.toString()}`; } - - // Template url: https://github.com/CATcher-org/templates#dev-response-phase - createGithubTeamResponse(): string { - return ( - `# Team\'s Response\n${this.teamResponse}\n` + - `## Duplicate status (if any):\n${this.duplicateOf ? `Duplicate of #${this.duplicateOf}` : `--`}` - ); - } - - // Template url: https://github.com/CATcher-org/templates#tutor-moderation - createGithubTutorResponse(): string { - let tutorResponseString = '# Tutor Moderation\n\n'; - for (const issueDispute of this.issueDisputes) { - tutorResponseString += issueDispute.toTutorResponseString(); - } - return tutorResponseString; - } - - // Template url: https://github.com/CATcher-org/templates#teams-response-1 - createGithubTesterResponse(): string { - return ( - `# Team\'s Response\n${this.teamResponse}\n` + - `# Items for the Tester to Verify\n${this.getTesterResponsesString(this.testerResponses)}` - ); - } - - /** - * Gets the number of unresolved disputes in an Issue. - */ - numOfUnresolvedDisputes(): number { - if (!this.issueDisputes) { - return 0; - } - - return this.issueDisputes.reduce((prev, current) => prev + Number(!current.isDone()), 0); - } - - private getTesterResponsesString(testerResponses: TesterResponse[]): string { - let testerResponsesString = ''; - for (const testerResponse of testerResponses) { - testerResponsesString += testerResponse.toString(); - } - return testerResponsesString; - } } export interface Issues { [id: number]: Issue; } -export const SEVERITY_ORDER = { '-': 0, VeryLow: 1, Low: 2, Medium: 3, High: 4 }; - -export const ISSUE_TYPE_ORDER = { '-': 0, DocumentationBug: 1, FeatureFlaw: 2, FunctionalityBug: 3 }; - -export enum STATUS { - Incomplete = 'Incomplete', - Done = 'Done' -} - export const IssuesFilter = { issuesViewer: { Student: 'NO_FILTER', diff --git a/src/app/core/models/label.model.ts b/src/app/core/models/label.model.ts index a2e6350e4..54c281fd6 100644 --- a/src/app/core/models/label.model.ts +++ b/src/app/core/models/label.model.ts @@ -1,7 +1,7 @@ /** * 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. @@ -24,7 +24,7 @@ export class Label { /** * Represents a simplified label with name and color */ -export type SimplifiedLabel = { +export type SimpleLabel = { name: string; color: string; }; diff --git a/src/app/core/models/parser/admins.model.ts b/src/app/core/models/parser/admins.model.ts deleted file mode 100644 index 9d6ddd8aa..000000000 --- a/src/app/core/models/parser/admins.model.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Admins { - [name: string]: Object; -} diff --git a/src/app/core/models/parser/parsed-user-data.model.ts b/src/app/core/models/parser/parsed-user-data.model.ts deleted file mode 100644 index dc8f64073..000000000 --- a/src/app/core/models/parser/parsed-user-data.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ParsedUserData { - role?: string; - name?: string; - team?: string; -} diff --git a/src/app/core/models/parser/roles.model.ts b/src/app/core/models/parser/roles.model.ts deleted file mode 100644 index 45205eb14..000000000 --- a/src/app/core/models/parser/roles.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface Roles { - students?: { - [loginId: string]: string; - }; - tutors?: { - [loginId: string]: string; - }; - admins?: { - [loginId: string]: string; - }; -} diff --git a/src/app/core/models/parser/students.model.ts b/src/app/core/models/parser/students.model.ts deleted file mode 100644 index 9e72226a8..000000000 --- a/src/app/core/models/parser/students.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Students { - [studentname: string]: { - teamId?: string; - }; -} diff --git a/src/app/core/models/parser/tabulated-user-data.model.ts b/src/app/core/models/parser/tabulated-user-data.model.ts deleted file mode 100644 index 9e195b2ab..000000000 --- a/src/app/core/models/parser/tabulated-user-data.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Admins } from './admins.model'; -import { Roles } from './roles.model'; -import { Students } from './students.model'; -import { Teams } from './teams.model'; -import { Tutors } from './tutors.model'; - -export interface TabulatedUserData { - 'admins-allocation'?: Admins; - roles?: Roles; - 'students-allocation'?: Students; - 'team-structure'?: Teams; - 'tutors-allocation'?: Tutors; -} diff --git a/src/app/core/models/parser/teams.model.ts b/src/app/core/models/parser/teams.model.ts deleted file mode 100644 index 5ac634f15..000000000 --- a/src/app/core/models/parser/teams.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Teams { - [teamId: string]: { - [teamMember: string]: string; - }; -} diff --git a/src/app/core/models/parser/tutors.model.ts b/src/app/core/models/parser/tutors.model.ts deleted file mode 100644 index 9c70a9e1f..000000000 --- a/src/app/core/models/parser/tutors.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Tutors { - [name: string]: { - [tutorialGroup: string]: string; - }; -} diff --git a/src/app/core/models/templates/sections/common-parsers.model.ts b/src/app/core/models/templates/sections/common-parsers.model.ts deleted file mode 100644 index 9ab039e08..000000000 --- a/src/app/core/models/templates/sections/common-parsers.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -const { char, choice, coroutine, everyCharUntil, str, whitespace } = require('arcsecond'); - -const TEAM_RESPONSE_HEADER = "# Team's Response"; - -export function buildTeamResponseSectionParser(nextHeader: string) { - return coroutine(function* () { - yield str(TEAM_RESPONSE_HEADER); - yield whitespace; - const teamResponse = yield everyCharUntil(str(nextHeader)); - - return teamResponse.trim(); - }); -} - -export function buildCheckboxParser(description: string) { - return coroutine(function* () { - yield str('- ['); - const checkbox = yield choice([char('x'), whitespace]); - yield str('] ' + description); - - return checkbox === 'x'; - }); -} diff --git a/src/app/core/models/templates/sections/duplicate-of-section.model.ts b/src/app/core/models/templates/sections/duplicate-of-section.model.ts deleted file mode 100644 index 86f7f9fa0..000000000 --- a/src/app/core/models/templates/sections/duplicate-of-section.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Section, SectionalDependency } from './section.model'; - -export class DuplicateOfSection extends Section { - private readonly duplicateOfRegex = /Duplicate of\s*#(\d+)/i; - issueNumber: number; - - constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) { - super(sectionalDependency, unprocessedContent); - if (!this.parseError) { - this.issueNumber = this.parseDuplicateOfValue(this.content); - } - } - - private parseDuplicateOfValue(toParse): number { - const result = this.duplicateOfRegex.exec(toParse); - return result ? +result[1] : null; - } - - toString(): string { - let toString = ''; - toString += `${this.header}\n`; - toString += this.parseError ? '--' : `Duplicate of ${this.issueNumber}\n`; - return toString; - } -} diff --git a/src/app/core/models/templates/sections/issue-dispute-section-parser.model.ts b/src/app/core/models/templates/sections/issue-dispute-section-parser.model.ts deleted file mode 100644 index 554e19b13..000000000 --- a/src/app/core/models/templates/sections/issue-dispute-section-parser.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IssueDispute } from '../../issue-dispute.model'; - -const { coroutine, everyCharUntil, optionalWhitespace, str } = require('arcsecond'); - -const SECTION_TITLE_PREFIX = '## :question: '; -const TEAM_SAYS_HEADER = '### Team says:'; -const LINE_SEPARATOR = '-------------------'; - -export const IssueDisputeSectionParser = coroutine(function* () { - yield str(SECTION_TITLE_PREFIX); - const title = yield everyCharUntil(str(TEAM_SAYS_HEADER)); - - const description = yield everyCharUntil(str(LINE_SEPARATOR)); - yield str(LINE_SEPARATOR); - yield optionalWhitespace; - - return new IssueDispute(title.trim(), description.trim()); -}); diff --git a/src/app/core/models/templates/sections/issue-dispute-section.model.ts b/src/app/core/models/templates/sections/issue-dispute-section.model.ts deleted file mode 100644 index e99abeb28..000000000 --- a/src/app/core/models/templates/sections/issue-dispute-section.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IssueDispute } from '../../issue-dispute.model'; -import { Section, SectionalDependency } from './section.model'; - -export class IssueDisputeSection extends Section { - disputes: IssueDispute[] = []; - - constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) { - super(sectionalDependency, unprocessedContent); - if (!this.parseError) { - let matches; - const regex = /#{2} *:question: *(.*)[\r\n]*([\s\S]*?(?=-{19}))/gi; - while ((matches = regex.exec(this.content))) { - if (matches) { - const [_regexString, title, description] = matches; - this.disputes.push(new IssueDispute(title, description.trim())); - } - } - } - } - - toString(): string { - let toString = ''; - toString += `${this.header.toString()}\n`; - for (const dispute of this.disputes) { - toString += `${dispute.toString()}\n`; - } - return toString; - } -} diff --git a/src/app/core/models/templates/sections/moderation-section-parser.model.ts b/src/app/core/models/templates/sections/moderation-section-parser.model.ts deleted file mode 100644 index 8b06501d9..000000000 --- a/src/app/core/models/templates/sections/moderation-section-parser.model.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Checkbox } from '../../checkbox.model'; -import { IssueDispute } from '../../issue-dispute.model'; -import { buildCheckboxParser } from './common-parsers.model'; - -const { coroutine, everyCharUntil, lookAhead, optionalWhitespace, str, whitespace } = require('arcsecond'); - -const SECTION_TITLE_PREFIX = '## :question: '; -const DONE_CHECKBOX_DESCRIPTION = 'Done'; -const LINE_SEPARATOR = '-------------------'; - -export const DoneCheckboxParser = buildCheckboxParser(DONE_CHECKBOX_DESCRIPTION); - -export const ModerationSectionParser = coroutine(function* () { - yield str(SECTION_TITLE_PREFIX); - const title = yield everyCharUntil(str('- [')); // every char until the done checkbox - - const description = yield lookAhead(everyCharUntil(str(LINE_SEPARATOR))); - - const doneCheckboxValue = yield DoneCheckboxParser; - yield whitespace; - const tutorResponse = yield everyCharUntil(str(LINE_SEPARATOR)); - yield str(LINE_SEPARATOR); - yield optionalWhitespace; - - const dispute = new IssueDispute(title.trim(), description.trim()); - dispute.todo = new Checkbox(DONE_CHECKBOX_DESCRIPTION, doneCheckboxValue); - dispute.tutorResponse = tutorResponse.trim(); - - return dispute; -}); diff --git a/src/app/core/models/templates/sections/moderation-section.model.ts b/src/app/core/models/templates/sections/moderation-section.model.ts deleted file mode 100644 index edde99a27..000000000 --- a/src/app/core/models/templates/sections/moderation-section.model.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Checkbox } from '../../checkbox.model'; -import { IssueDispute } from '../../issue-dispute.model'; -import { Section, SectionalDependency } from './section.model'; - -export class ModerationSection extends Section { - disputesToResolve: IssueDispute[] = []; - - constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) { - super(sectionalDependency, unprocessedContent); - if (!this.parseError) { - let matches; - const regex = /#{2} *:question: *(.*)[\n\r]*(.*)[\n\r]*([\s\S]*?(?=-{19}))/gi; - while ((matches = regex.exec(this.content))) { - if (matches) { - const [_regexString, title, todo, tutorResponse] = matches; - const description = `${todo}\n${tutorResponse}`; - const newDispute = new IssueDispute(title, description); - - newDispute.todo = new Checkbox(todo, false); - newDispute.tutorResponse = tutorResponse.trim(); - this.disputesToResolve.push(newDispute); - } - } - } - } - - get todoList(): Checkbox[] { - return this.disputesToResolve.map((e) => e.todo); - } - - toString(): string { - let toString = ''; - toString += `${this.header.toString()}\n`; - for (const dispute of this.disputesToResolve) { - toString += `${dispute.toTutorResponseString()}\n`; - } - return toString; - } -} diff --git a/src/app/core/models/templates/sections/section.model.ts b/src/app/core/models/templates/sections/section.model.ts deleted file mode 100644 index 1d6bf3a7a..000000000 --- a/src/app/core/models/templates/sections/section.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * A SectionalDependency defines a format that is needed to create a successful Section in a template. - * It will require the Section's header to be defined and the other headers that are present in the template. - * - * Reason for the dependencies on other headers: We need them to create a regex expression that is capable of parsing the current - * section out of the string. - */ -import { Header } from '../template.model'; - -export interface SectionalDependency { - sectionHeader: Header; - remainingTemplateHeaders: Header[]; -} - -export class Section { - header: Header; - sectionRegex: RegExp; - content: string; - parseError: string; - - /** - * - * @param sectionalDependency The dependency that is need to create a section's regex - * @param unprocessedContent The string that stores the section's amongst other things - */ - constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) { - this.header = sectionalDependency.sectionHeader; - // If length === 0, match till end of string else match till regex hits another section - const matchTillRegex = - sectionalDependency.remainingTemplateHeaders.length === 0 ? '$' : sectionalDependency.remainingTemplateHeaders.join('|'); - this.sectionRegex = new RegExp(`(${this.header})\\s+([\\s\\S]*?)(?=${matchTillRegex}|$)`, 'i'); - const matches = this.sectionRegex.exec(unprocessedContent); - if (matches) { - const [_originalString, _header, description] = matches; - this.content = description.trim(); - this.parseError = null; - } else { - this.content = null; - this.parseError = `Unable to extract ${this.header.name} Section`; - } - } -} diff --git a/src/app/core/models/templates/sections/tester-response-section-parser.model.ts b/src/app/core/models/templates/sections/tester-response-section-parser.model.ts deleted file mode 100644 index 9c7ef10db..000000000 --- a/src/app/core/models/templates/sections/tester-response-section-parser.model.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { buildCheckboxParser } from './common-parsers.model'; - -const { - between, - coroutine, - everyCharUntil, - letters, - lookAhead, - optionalWhitespace, - pipeParsers, - possibly, - str, - whitespace -} = require('arcsecond'); - -const SECTION_TITLE_PREFIX = '## :question: Issue '; -const TEAM_CHOSE_PREFIX = 'Team chose '; -const TESTER_CHOSE_PREFIX = 'Originally '; -const DISAGREE_CHECKBOX_DESCRIPTION = 'I disagree'; -const DISAGREEMENT_REASON_PREFIX = '**Reason for disagreement:** '; -const LINE_SEPARATOR = '-------------------'; -const DUPLICATE_STATUS_MESSAGE = - "Team chose to mark this issue as a duplicate of another issue (as explained in the _**Team's response**_ above)"; - -export const DisagreeCheckboxParser = buildCheckboxParser(DISAGREE_CHECKBOX_DESCRIPTION); - -function buildExtractResponseParser(category: string) { - return between(str('[`' + category + '.'))(str('`]'))(letters); -} - -function buildTeamResponseParser(category: string) { - const extractResponseParser = buildExtractResponseParser(category); - - return pipeParsers([str(TEAM_CHOSE_PREFIX), extractResponseParser]); -} - -function buildTesterResponseParser(category: string) { - const extractResponseParser = buildExtractResponseParser(category); - - return pipeParsers([str(TESTER_CHOSE_PREFIX), extractResponseParser]); -} - -export const DisagreeReasonParser = coroutine(function* () { - yield str(DISAGREEMENT_REASON_PREFIX); - const reasonForDisagreement = yield everyCharUntil(str(LINE_SEPARATOR)); - yield str(LINE_SEPARATOR); - - return reasonForDisagreement.trim(); -}); - -// Issue duplicate section has a different format than the other three -const DuplicateSectionParser = coroutine(function* () { - yield str('status'); - yield whitespace; - yield str(DUPLICATE_STATUS_MESSAGE); - yield whitespace; - - const disagreeCheckboxValue = yield DisagreeCheckboxParser; - yield whitespace; - const reasonForDisagreement = yield DisagreeReasonParser; - - return { - disagreeCheckboxValue: disagreeCheckboxValue, - reasonForDisagreement: reasonForDisagreement - }; -}); - -export const TesterResponseSectionParser = coroutine(function* () { - // section title - yield str(SECTION_TITLE_PREFIX); - const title = yield letters; - yield whitespace; - - if (title === 'duplicate') { - const dupSectionResult = yield DuplicateSectionParser; - yield optionalWhitespace; - - return { - title: title + ' status', - description: DUPLICATE_STATUS_MESSAGE, - teamChose: null, - testerChose: null, - disagreeCheckboxValue: dupSectionResult.disagreeCheckboxValue, - reasonForDisagreement: dupSectionResult.reasonForDisagreement - }; - } - - const description = yield lookAhead(everyCharUntil(DisagreeCheckboxParser)); - - // team and tester response - const teamResponseParser = buildTeamResponseParser(title); - const testerResponseParser = buildTesterResponseParser(title); - - const teamChose = yield teamResponseParser; - yield whitespace; - // response section does not have tester response - const testerChose = yield possibly(testerResponseParser); - yield optionalWhitespace; - - const disagreeCheckboxValue = yield DisagreeCheckboxParser; - yield whitespace; - const reasonForDisagreement = yield DisagreeReasonParser; - yield optionalWhitespace; - - return { - title: title, - description: description.trim(), - teamChose: teamChose, - testerChose: testerChose, - disagreeCheckboxValue: disagreeCheckboxValue, - reasonForDisagreement: reasonForDisagreement - }; -}); diff --git a/src/app/core/models/templates/sections/tester-response-section.model.ts b/src/app/core/models/templates/sections/tester-response-section.model.ts deleted file mode 100644 index e6809bef2..000000000 --- a/src/app/core/models/templates/sections/tester-response-section.model.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { extractStringBetween } from '../../../../shared/lib/string-utils'; -import { TesterResponse } from '../../tester-response.model'; -import { Section, SectionalDependency } from './section.model'; - -// match format e.g. ## :question: Issue Title -const matchTitle = '#{2} *:question: *([\\w ]+)'; -// match format e.g. Team Chose severity.Low \r\n Originally (or Team Chose) severity.High \r\n -const matchDescription = '(Team Chose.*[\\r\\n]* *Originally.*|Team Chose.*[\\r\\n]*)'; -// match format e.g. - [x] (or - [ ]) **Reason for disagreement:** disagreement explanation -const matchDisagreement = '(- \\[x? ?\\] I disagree)[\\r\\n]*\\*\\*Reason *for *disagreement:\\*\\* *([\\s\\S]*?)'; -const matchLinebreak = '[\\n\\r]-{19}'; - -export class TesterResponseSection extends Section { - testerResponses: TesterResponse[] = []; - testerDisagree: boolean; - teamChosenSeverity?: string; - teamChosenType?: string; - - ISSUE_SEVERITY_DISPUTE = 'Issue severity'; - ISSUE_TYPE_DISPUTE = 'Issue type'; - TEAM_RESPONSE_DESCRIPTION_TYPE_VALUE_PREFIX = '[`type.'; - TEAM_RESPONSE_DESCRIPTION_SEVERITY_VALUE_PREFIX = '[`severity.'; - TEAM_RESPONSE_DESCRIPTION_VALUE_SUFFIX = '`]'; - - constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) { - super(sectionalDependency, unprocessedContent); - if (!this.parseError) { - let matches; - const regex: RegExp = new RegExp([matchTitle, matchDescription, matchDisagreement].join('[\\r\\n]*') + matchLinebreak, 'gi'); - while ((matches = regex.exec(this.content))) { - if (matches) { - const [_, title, description, disagreeCheckbox, reasonForDisagreement] = matches; - - if (this.isSeverityDispute(title)) { - this.teamChosenSeverity = this.parseTeamChosenSeverity(description); - } else if (this.isTypeDispute(title)) { - this.teamChosenType = this.parseTeamChosenType(description); - } - - const disagreeCheckboxValue = this.parseCheckboxValue(disagreeCheckbox); - if (disagreeCheckboxValue) { - this.testerDisagree = true; // on any disagree, overall disagree with team response - } - - this.testerResponses.push( - new TesterResponse( - title, - description, - this.parseCheckboxDescription(disagreeCheckbox), - disagreeCheckboxValue, - reasonForDisagreement.trim() - ) - ); - } - } - } - } - - isSeverityDispute(title: string): boolean { - return title.trim() === this.ISSUE_SEVERITY_DISPUTE; - } - - isTypeDispute(title: string): boolean { - return title.trim() === this.ISSUE_TYPE_DISPUTE; - } - - getTeamChosenType(): string { - return this.teamChosenType; - } - - getTeamChosenSeverity(): string { - return this.teamChosenSeverity; - } - - getTesterDisagree(): boolean { - return this.testerDisagree; - } - - parseTeamChosenSeverity(description: string): string { - return extractStringBetween( - description, - this.TEAM_RESPONSE_DESCRIPTION_SEVERITY_VALUE_PREFIX, - this.TEAM_RESPONSE_DESCRIPTION_VALUE_SUFFIX - ); - } - - parseTeamChosenType(description: string): string { - return extractStringBetween(description, this.TEAM_RESPONSE_DESCRIPTION_TYPE_VALUE_PREFIX, this.TEAM_RESPONSE_DESCRIPTION_VALUE_SUFFIX); - } - - parseCheckboxValue(checkboxString: string): boolean { - return checkboxString.charAt(3) === 'x'; // checkboxString in the format of - [x] or - [ ] - } - - parseCheckboxDescription(checkboxString: string): string { - return checkboxString.substring(6).trim(); // checkboxString has a fixed 5 characters at the start before the description - } - - toString(): string { - let toString = ''; - toString += `${this.header.toString()}\n`; - for (const response of this.testerResponses) { - toString += `${response.toString()}\n`; - } - return toString; - } -} diff --git a/src/app/core/models/templates/team-accepted-template.model.ts b/src/app/core/models/templates/team-accepted-template.model.ts deleted file mode 100644 index b208fd39d..000000000 --- a/src/app/core/models/templates/team-accepted-template.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GithubComment } from '../github/github-comment.model'; -import { Header, Template } from './template.model'; - -const { endOfInput, sequenceOf, startOfInput, str } = require('arcsecond'); - -export const TeamAcceptedMessage = 'Your response not required for this bug as the team has accepted the bug as it is.'; -export const TeamAcceptedHeader = { teamAccepted: new Header(TeamAcceptedMessage, 0) }; - -const TeamAcceptedParser = sequenceOf([startOfInput, str(TeamAcceptedMessage), endOfInput]); - -export class TeamAcceptedTemplate extends Template { - teamAccepted?: boolean; - - constructor(githubComments: GithubComment[]) { - super(TeamAcceptedParser, Object.values(TeamAcceptedHeader)); - - this.findConformingComment(githubComments); - - if (this.parseFailure) { - return; - } - - this.teamAccepted = true; - } -} diff --git a/src/app/core/models/templates/team-response-template.model.ts b/src/app/core/models/templates/team-response-template.model.ts deleted file mode 100644 index 6f74aa0f7..000000000 --- a/src/app/core/models/templates/team-response-template.model.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { IssueComment } from '../comment.model'; -import { GithubComment } from '../github/github-comment.model'; -import { buildTeamResponseSectionParser } from './sections/common-parsers.model'; -import { DuplicateOfSection } from './sections/duplicate-of-section.model'; -import { Section } from './sections/section.model'; -import { Header, Template } from './template.model'; - -const { choice, coroutine, digits, str, whitespace } = require('arcsecond'); - -export const TeamResponseHeaders = { - teamResponse: new Header("Team's Response", 1), - duplicateOf: new Header('Duplicate status \\(if any\\):', 2) -}; - -interface TeamResponseParseResult { - teamResponse: string; - issueNumber: number; -} - -const DUPLICATE_OF_HEADER = '## Duplicate status (if any):'; - -const TeamResponseSectionParser = buildTeamResponseSectionParser(DUPLICATE_OF_HEADER); - -const DuplicateNumberParser = coroutine(function* () { - yield str('Duplicate of #'); // parse and ignore - const issueNumber = yield digits; // parse and store duplicate issue number - return parseInt(issueNumber, 10); // issueNumber is a string, radix added to pass linting -}); - -export const TeamResponseParser = coroutine(function* () { - const teamResponse = yield TeamResponseSectionParser; - - yield str(DUPLICATE_OF_HEADER); - yield whitespace; - const issueNumber = yield choice([ - // either parse duplicate issue number or '--' if no duplicates - DuplicateNumberParser, - str('--') - ]).map((num) => (num === '--' ? null : num)); - - const result: TeamResponseParseResult = { - teamResponse: teamResponse, - issueNumber: issueNumber - }; - return result; -}); - -export class TeamResponseTemplate extends Template { - teamResponse: Section; - duplicateOf: DuplicateOfSection; - comment: IssueComment; - - constructor(githubComments: GithubComment[]) { - super(TeamResponseParser, Object.values(TeamResponseHeaders)); - - const templateConformingComment = this.findConformingComment(githubComments); - - if (this.parseFailure) { - return; - } - - this.comment = { - ...templateConformingComment, - description: templateConformingComment.body, - createdAt: templateConformingComment.created_at, - updatedAt: templateConformingComment.updated_at - }; - const commentsContent: string = templateConformingComment.body; - this.teamResponse = this.parseTeamResponse(commentsContent); - this.duplicateOf = this.parseDuplicateOf(commentsContent); - } - - parseTeamResponse(toParse: string): Section { - return new Section(this.getSectionalDependency(TeamResponseHeaders.teamResponse), toParse); - } - - parseDuplicateOf(toParse: string): DuplicateOfSection { - return new DuplicateOfSection(this.getSectionalDependency(TeamResponseHeaders.duplicateOf), toParse); - } -} diff --git a/src/app/core/models/templates/template.model.ts b/src/app/core/models/templates/template.model.ts deleted file mode 100644 index d6d349b35..000000000 --- a/src/app/core/models/templates/template.model.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { GithubComment } from '../github/github-comment.model'; -import { SectionalDependency } from './sections/section.model'; - -export abstract class Template { - headers: Header[]; - parser; - parseFailure: boolean; - - protected constructor(parser, headers: Header[]) { - this.parser = parser; - this.headers = headers; - } - - getSectionalDependency(header: Header): SectionalDependency { - const otherHeaders = this.headers.filter((e) => !e.equals(header)); - return { - sectionHeader: header, - remainingTemplateHeaders: otherHeaders - }; - } - - test(toTest: string): boolean { - return !this.parser.run(toTest).isError; - } - - /** - * Finds a comment that conforms to the template - */ - findConformingComment(githubComments: GithubComment[]): GithubComment { - const templateConformingComment = githubComments.find((githubComment) => this.test(githubComment.body)); - if (templateConformingComment === undefined) { - this.parseFailure = true; - } - return templateConformingComment; - } -} - -export class Header { - name: string; - headerHash: string; - prefix?: string; - - constructor(name, headerSize, prefix: string = '') { - this.name = name; - this.headerHash = '#'.repeat(headerSize); - this.prefix = prefix; - } - - toString(): string { - const prefix = this.prefix !== '' ? this.prefix + ' ' : ''; - const headerHashPrefix = this.headerHash !== '' ? this.headerHash + ' ' : ''; - return `${headerHashPrefix}${prefix}${this.name}`; - } - - equals(section: Header): boolean { - return this.name === section.name; - } -} diff --git a/src/app/core/models/templates/tester-response-template.model.ts b/src/app/core/models/templates/tester-response-template.model.ts deleted file mode 100644 index fc0e90184..000000000 --- a/src/app/core/models/templates/tester-response-template.model.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { IssueComment } from '../comment.model'; -import { GithubComment } from '../github/github-comment.model'; -import { TesterResponse } from '../tester-response.model'; -import { buildTeamResponseSectionParser } from './sections/common-parsers.model'; -import { Section } from './sections/section.model'; -import { TesterResponseSectionParser } from './sections/tester-response-section-parser.model'; -import { TesterResponseSection } from './sections/tester-response-section.model'; -import { Header, Template } from './template.model'; - -const { coroutine, many1, str, whitespace } = require('arcsecond'); - -export const TesterResponseHeaders = { - teamResponse: new Header("Team's Response", 1), - testerResponses: new Header('Items for the Tester to Verify', 1) -}; - -interface TesterResponseParseResult { - teamResponse: string; - testerResponses: TesterResponse[]; - testerDisagree: boolean; - teamChosenSeverity: string; - teamChosenType: string; -} - -const TESTER_RESPONSES_HEADER = '# Items for the Tester to Verify'; -const DISAGREE_CHECKBOX_DESCRIPTION = 'I disagree'; - -const TeamResponseSectionParser = buildTeamResponseSectionParser(TESTER_RESPONSES_HEADER); - -export const TesterResponseParser = coroutine(function* () { - const teamResponse = yield TeamResponseSectionParser; - - // parse tester responses from comment - yield str(TESTER_RESPONSES_HEADER); - yield whitespace; - const responses = yield many1(TesterResponseSectionParser); - - // build array of TesterResponse - let testerDisagree = false; - let teamChosenSeverity: string; - let teamChosenType: string; - const testerResponses: TesterResponse[] = []; - - for (const response of responses) { - if (response.disagreeCheckboxValue) { - testerDisagree = true; - } - - if (response.title === 'severity') { - teamChosenSeverity = response.teamChose; - } else if (response.title === 'type') { - teamChosenType = response.teamChose; - } - - testerResponses.push( - new TesterResponse( - 'Issue ' + response.title, - response.description, - DISAGREE_CHECKBOX_DESCRIPTION, - response.disagreeCheckboxValue, - response.reasonForDisagreement - ) - ); - } - - const result: TesterResponseParseResult = { - teamResponse: teamResponse, - testerResponses: testerResponses, - testerDisagree: testerDisagree, - teamChosenSeverity: teamChosenSeverity, - teamChosenType: teamChosenType - }; - return result; -}); - -export class TesterResponseTemplate extends Template { - teamResponse: Section; - testerResponse: TesterResponseSection; - testerDisagree: boolean; - comment: IssueComment; - teamChosenSeverity?: string; - teamChosenType?: string; - - constructor(githubComments: GithubComment[]) { - super(TesterResponseParser, Object.values(TesterResponseHeaders)); - - const templateConformingComment = this.findConformingComment(githubComments); - - if (this.parseFailure) { - return; - } - - this.comment = { - ...templateConformingComment, - description: templateConformingComment.body - }; - this.teamResponse = this.parseTeamResponse(this.comment.description); - this.testerResponse = this.parseTesterResponse(this.comment.description); - this.testerDisagree = this.testerResponse.getTesterDisagree(); - this.teamChosenSeverity = this.testerResponse.getTeamChosenSeverity(); - this.teamChosenType = this.testerResponse.getTeamChosenType(); - } - - parseTeamResponse(toParse: string): Section { - return new Section(this.getSectionalDependency(TesterResponseHeaders.teamResponse), toParse); - } - - parseTesterResponse(toParse: string): TesterResponseSection { - return new TesterResponseSection(this.getSectionalDependency(TesterResponseHeaders.testerResponses), toParse); - } -} diff --git a/src/app/core/models/templates/tutor-moderation-issue-template.model.ts b/src/app/core/models/templates/tutor-moderation-issue-template.model.ts deleted file mode 100644 index c5539437c..000000000 --- a/src/app/core/models/templates/tutor-moderation-issue-template.model.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { GithubIssue } from '../github/github-issue.model'; -import { IssueDispute } from '../issue-dispute.model'; -import { buildTeamResponseSectionParser } from './sections/common-parsers.model'; -import { IssueDisputeSectionParser } from './sections/issue-dispute-section-parser.model'; -import { IssueDisputeSection } from './sections/issue-dispute-section.model'; -import { Section } from './sections/section.model'; -import { Header, Template } from './template.model'; - -const { coroutine, everyCharUntil, many1, str, whitespace } = require('arcsecond'); - -const tutorModerationIssueDescriptionHeaders = { - description: new Header('Issue Description', 1), - teamResponse: new Header("Team's Response", 1), - disputes: new Header('Disputes', 1) -}; - -interface TutorModerationIssueParseResult { - description: string; - teamResponse: string; - issueDisputes: IssueDispute[]; -} - -const DESCRIPTION_HEADER = '# Issue Description'; -const TEAM_RESPONSE_HEADER = "# Team's Response"; -const DISPUTES_HEADER = '# Disputes'; - -const TeamResponseSectionParser = buildTeamResponseSectionParser(DISPUTES_HEADER); - -export const TutorModerationIssueParser = coroutine(function* () { - yield str(DESCRIPTION_HEADER); - yield whitespace; - const description = yield everyCharUntil(str(TEAM_RESPONSE_HEADER)); - - const teamResponse = yield TeamResponseSectionParser; - - // parse disputes - yield str(DISPUTES_HEADER); - yield whitespace; - const disputes = yield many1(IssueDisputeSectionParser); - - const result: TutorModerationIssueParseResult = { - description: description.trim(), - teamResponse: teamResponse, - issueDisputes: disputes - }; - return result; -}); - -export class TutorModerationIssueTemplate extends Template { - description: Section; - teamResponse: Section; - dispute: IssueDisputeSection; - - constructor(githubIssue: GithubIssue) { - super(TutorModerationIssueParser, Object.values(tutorModerationIssueDescriptionHeaders)); - - const issueContent = githubIssue.body; - this.description = this.parseDescription(issueContent); - this.teamResponse = this.parseTeamResponse(issueContent); - this.dispute = this.parseDisputes(issueContent); - } - - parseDescription(toParse: string): Section { - return new Section(this.getSectionalDependency(tutorModerationIssueDescriptionHeaders.description), toParse); - } - - parseTeamResponse(toParse: string): Section { - return new Section(this.getSectionalDependency(tutorModerationIssueDescriptionHeaders.teamResponse), toParse); - } - - parseDisputes(toParse: string): IssueDisputeSection { - return new IssueDisputeSection(this.getSectionalDependency(tutorModerationIssueDescriptionHeaders.disputes), toParse); - } -} diff --git a/src/app/core/models/templates/tutor-moderation-todo-template.model.ts b/src/app/core/models/templates/tutor-moderation-todo-template.model.ts deleted file mode 100644 index 81e3669d1..000000000 --- a/src/app/core/models/templates/tutor-moderation-todo-template.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IssueComment } from '../comment.model'; -import { GithubComment } from '../github/github-comment.model'; -import { IssueDispute } from '../issue-dispute.model'; -import { ModerationSectionParser } from './sections/moderation-section-parser.model'; -import { ModerationSection } from './sections/moderation-section.model'; -import { Header, Template } from './template.model'; - -const { coroutine, many1, str, whitespace } = require('arcsecond'); - -const tutorModerationTodoHeaders = { - todo: new Header('Tutor Moderation', 1) -}; - -interface TutorModerationTodoParseResult { - disputesToResolve: IssueDispute[]; -} - -const TODO_HEADER = '# Tutor Moderation'; - -export const TutorModerationTodoParser = coroutine(function* () { - yield str(TODO_HEADER); - yield whitespace; - - const tutorResponses = yield many1(ModerationSectionParser); - - const result: TutorModerationTodoParseResult = { - disputesToResolve: tutorResponses - }; - return result; -}); - -export class TutorModerationTodoTemplate extends Template { - moderation: ModerationSection; - comment: IssueComment; - - constructor(githubComments: GithubComment[]) { - super(TutorModerationTodoParser, Object.values(tutorModerationTodoHeaders)); - - const templateConformingComment = this.findConformingComment(githubComments); - - if (this.parseFailure) { - return; - } - - this.comment = { - ...templateConformingComment, - description: templateConformingComment.body - }; - this.moderation = this.parseModeration(this.comment.description); - } - - parseModeration(toParse: string): ModerationSection { - return new ModerationSection(this.getSectionalDependency(tutorModerationTodoHeaders.todo), toParse); - } -} diff --git a/src/app/core/models/tester-response.model.ts b/src/app/core/models/tester-response.model.ts deleted file mode 100644 index 3f761b453..000000000 --- a/src/app/core/models/tester-response.model.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Checkbox } from './checkbox.model'; - -export class TesterResponse { - readonly TITLE_PREFIX = '## :question: '; - readonly DISAGREEMENT_PREFIX = '**Reason for disagreement:** '; - readonly INITIAL_RESPONSE = '[replace this with your explanation]'; - readonly LINE_BREAK = '-------------------\n'; - title: string; // e.g Issue Severity - description: string; // e.g Team chose `Low`. Originally `High`. - disagreeCheckbox: Checkbox; // e.g - [x] I disagree - reasonForDisagreement: string; - - constructor(title: string, description: string, checkboxDescription: string, isChecked: boolean, reasonForDiagreement: string) { - this.title = title; - this.description = description; - this.disagreeCheckbox = new Checkbox(checkboxDescription, isChecked); - this.reasonForDisagreement = reasonForDiagreement; - } - - toString(): string { - let toString = ''; - toString += this.TITLE_PREFIX + this.title + '\n\n'; - toString += this.description + '\n\n'; - toString += this.disagreeCheckbox.toString() + '\n\n'; - toString += this.DISAGREEMENT_PREFIX + this.reasonForDisagreement + '\n\n'; - toString += this.LINE_BREAK; - return toString; - } - - isDisagree(): boolean { - return this.disagreeCheckbox.isChecked; - } - - compareTo(anotherResponse: TesterResponse): number { - if (this.isDisagree() === anotherResponse.isDisagree()) { - return this.reasonForDisagreement.localeCompare(anotherResponse.reasonForDisagreement); - } - return this.isDisagree() ? 1 : -1; - } - - getTitleInMarkDown(): string { - return `## ${this.title}`; - } - - getDisagreementWithoutDefaultResponse(): string { - return this.reasonForDisagreement.replace(this.INITIAL_RESPONSE, ' '); - } - - setDisagree(isDisagree: boolean) { - this.disagreeCheckbox.setChecked(isDisagree); - } - - setReasonForDisagreement(reason: string) { - this.reasonForDisagreement = reason; - } -} diff --git a/src/app/core/models/timeline-item.model.ts b/src/app/core/models/timeline-item.model.ts deleted file mode 100644 index 9613a374d..000000000 --- a/src/app/core/models/timeline-item.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type TimelineTime = { - starting_time: number; - ending_time: number; - display?: string; // circle/rect - id?: number; -}; - -export type TimelineItem = { - times: TimelineTime[]; - label?: string; - icon?: string; // path to image -}; diff --git a/src/app/core/services/issue.service.ts b/src/app/core/services/issue.service.ts index 76a22c3a9..1ad1aed9f 100644 --- a/src/app/core/services/issue.service.ts +++ b/src/app/core/services/issue.service.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, EMPTY, forkJoin, Observable, of, Subscription, throwEr import { catchError, exhaustMap, finalize, flatMap, 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, STATUS } from '../models/issue.model'; +import { Issue, Issues, IssuesFilter } from '../models/issue.model'; import { Phase } from '../models/phase.model'; import { GithubService } from './github.service'; import { PhaseService } from './phase.service'; diff --git a/src/app/core/services/label.service.ts b/src/app/core/services/label.service.ts index 9bc7cac0d..edeac7d7c 100644 --- a/src/app/core/services/label.service.ts +++ b/src/app/core/services/label.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, EMPTY, Observable, of, Subscription, timer } from 'rxjs'; import { catchError, exhaustMap, finalize, map } from 'rxjs/operators'; -import { Label, SimplifiedLabel } from '../models/label.model'; +import { Label, SimpleLabel } from '../models/label.model'; import { GithubService } from './github.service'; /* The threshold to decide if color is dark or light. @@ -25,10 +25,10 @@ export class LabelService { static readonly POLL_INTERVAL = 5000; // 5 seconds labels: Label[]; - simplifiedLabels: SimplifiedLabel[]; + simpleLabels: SimpleLabel[]; private labelsPollSubscription: Subscription; - private labelsSubject = new BehaviorSubject([]); + private labelsSubject = new BehaviorSubject([]); constructor(private githubService: GithubService) {} @@ -47,7 +47,7 @@ export class LabelService { }) ) .subscribe(() => { - this.labelsSubject.next(this.simplifiedLabels); + this.labelsSubject.next(this.simpleLabels); }); } @@ -58,7 +58,7 @@ export class LabelService { } } - connect(): Observable { + connect(): Observable { return this.labelsSubject.asObservable(); } @@ -69,13 +69,8 @@ export class LabelService { return this.githubService.fetchAllLabels().pipe( map((response) => { this.labels = this.parseLabelData(response); - this.simplifiedLabels = this.labels.map((label) => { - return { - name: label.formattedName, - color: label.color - }; - }); - this.labelsSubject.next(this.simplifiedLabels); + this.simpleLabels = this.labels; + this.labelsSubject.next(this.simpleLabels); return response; }) ); @@ -138,7 +133,7 @@ export class LabelService { reset() { this.labels = undefined; - this.simplifiedLabels = undefined; + this.simpleLabels = undefined; this.stopPollLabels(); } } diff --git a/src/app/core/services/phase.service.ts b/src/app/core/services/phase.service.ts index dd4df8bfa..9147593f7 100644 --- a/src/app/core/services/phase.service.ts +++ b/src/app/core/services/phase.service.ts @@ -5,6 +5,7 @@ import { Repo } from '../models/repo.model'; import { SessionData } from '../models/session.model'; import { GithubService } from './github.service'; import { LoggingService } from './logging.service'; +import { RepoUrlCacheService } from './repo-url-cache.service'; export const SESSION_AVALIABILITY_FIX_FAILED = 'Session Availability Fix failed.'; @@ -57,7 +58,7 @@ export class PhaseService { public sessionData = STARTING_SESSION_DATA; // stores session data for the session - constructor(private githubService: GithubService, public logger: LoggingService) {} + constructor(private githubService: GithubService, private repoUrlCacheService: RepoUrlCacheService, public logger: LoggingService) {} /** * Sets the current main repository and additional repos if any. @@ -87,12 +88,7 @@ export class PhaseService { } this.setRepository(repo, this.otherRepos); - // Update autofill repository URL suggestions in localStorage - const suggestions: string[] = JSON.parse(window.localStorage.getItem('suggestions')) || []; - if (!suggestions.includes(repo.toString())) { - suggestions.push(repo.toString()); - window.localStorage.setItem('suggestions', JSON.stringify(suggestions)); - } + this.repoUrlCacheService.cache(repo.toString()); this.repoChanged$.next(repo); } diff --git a/src/app/core/services/repo-url-cache.service.ts b/src/app/core/services/repo-url-cache.service.ts new file mode 100644 index 000000000..d786d6cc4 --- /dev/null +++ b/src/app/core/services/repo-url-cache.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class RepoUrlCacheService { + static readonly KEY_NAME = 'suggestions'; + + suggestions: string[]; + + constructor() { + this.suggestions = JSON.parse(window.localStorage.getItem(RepoUrlCacheService.KEY_NAME)) || []; + } + + cache(repo: string): void { + // Update autofill repository URL suggestions in localStorage + if (!this.suggestions.includes(repo)) { + this.suggestions.push(repo); + window.localStorage.setItem(RepoUrlCacheService.KEY_NAME, JSON.stringify(this.suggestions)); + } + } + + getFilteredSuggestions(control: AbstractControl): Observable { + // Ref: https://v10.material.angular.io/components/autocomplete/overview + return control.valueChanges.pipe( + startWith(''), + map((value) => this.suggestions.filter((suggestion) => suggestion.toLowerCase().includes(value.toLowerCase()))) + ); + } +} diff --git a/src/app/issues-viewer/card-view/card-view.component.css b/src/app/issues-viewer/card-view/card-view.component.css index 34d5674f8..8664c87e0 100644 --- a/src/app/issues-viewer/card-view/card-view.component.css +++ b/src/app/issues-viewer/card-view/card-view.component.css @@ -10,9 +10,9 @@ display: flex; align-items: center; font-size: 12px; - text-align: left; + text-align: center; overflow: auto; - overflow-wrap: anywhere; + word-break: break-word; } .column-header .mat-card-title { @@ -35,7 +35,7 @@ div.column-header .mat-card-title { div.column-header { justify-content: center; position: relative; - z-index: 1; + z-index: 5; } div.column-header .mat-card-header { @@ -50,26 +50,62 @@ div.column-header .mat-card-header { margin-bottom: 2px; scrollbar-width: none; position: relative; - - /* Ref: https://css-scroll-shadows.vercel.app */ - background: - linear-gradient(white 33%, transparent), - linear-gradient(transparent, white 66%) 0 100%, - radial-gradient(farthest-side at 50% 0, rgba(0,0,0, 0.5), transparent), - radial-gradient(farthest-side at 50% 100%, rgba(0,0,0, 0.5), transparent) 0 100%; - background-color: transparent; - background-repeat: no-repeat; - background-attachment: local, local, scroll, scroll; - background-size: 100% 15px, 100% 15px, 100% 5px, 100% 5px; } .scrollable-container::-webkit-scrollbar { display: none; } -.issue-pr-cards { +/* Ref: https://css-scroll-shadows.vercel.app */ +.scrollable-container::before { + pointer-events: none; + content: ''; + position: absolute; + z-index: 2; + height: 6px; + width: 100%; + display: block; + background-image: linear-gradient(to bottom, white 66%, transparent); +} + +.scrollable-container::after { + pointer-events: none; + content: ''; + position: sticky; + z-index: 2; + top: 100%; + height: 6px; + width: 100%; + display: block; + background-image: linear-gradient(to top, white 66%, transparent); +} + +.scrollable-container-wrapper { position: relative; - z-index: -1; +} + +.scrollable-container-wrapper::before { + pointer-events: none; + content: ''; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + height: 5px; + background-image: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.5), transparent); +} + +.scrollable-container-wrapper::after { + pointer-events: none; + content: ''; + position: absolute; + z-index: 1; + bottom: 0; + left: 0; + right: 0; + height: 5px; + background-image: radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.5), transparent); } .loading-spinner { diff --git a/src/app/issues-viewer/card-view/card-view.component.html b/src/app/issues-viewer/card-view/card-view.component.html index 146c0b4ad..872080b8d 100644 --- a/src/app/issues-viewer/card-view/card-view.component.html +++ b/src/app/issues-viewer/card-view/card-view.component.html @@ -18,14 +18,15 @@ -
-
- +
+
+
+ +
+ + +
- - -
-
diff --git a/src/app/issues-viewer/issues-viewer.component.html b/src/app/issues-viewer/issues-viewer.component.html index 9bb21ca12..0964a62bd 100644 --- a/src/app/issues-viewer/issues-viewer.component.html +++ b/src/app/issues-viewer/issues-viewer.component.html @@ -1,11 +1,6 @@
-
- - +
+
diff --git a/src/app/issues-viewer/issues-viewer.module.ts b/src/app/issues-viewer/issues-viewer.module.ts index ca35689b7..537960c3f 100644 --- a/src/app/issues-viewer/issues-viewer.module.ts +++ b/src/app/issues-viewer/issues-viewer.module.ts @@ -1,5 +1,4 @@ import { NgModule } from '@angular/core'; -import { MarkdownModule } from 'ngx-markdown'; 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'; @@ -8,7 +7,7 @@ import { IssuesViewerRoutingModule } from './issues-viewer-routing.module'; import { IssuesViewerComponent } from './issues-viewer.component'; @NgModule({ - imports: [FilterBarModule, IssuesViewerRoutingModule, IssuesPrCardModule, SharedModule, MarkdownModule.forChild()], + imports: [FilterBarModule, IssuesViewerRoutingModule, IssuesPrCardModule, SharedModule], declarations: [IssuesViewerComponent, CardViewComponent], exports: [IssuesViewerComponent, CardViewComponent] }) diff --git a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.css b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.css index f361a38d5..03e92e4be 100644 --- a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.css +++ b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.css @@ -1,7 +1,16 @@ +::ng-deep.mat-menu-content:not(:empty) { + /* Override mat-menu-content's non-empty default CSS. */ + padding-top: 0px !important; + padding-bottom: 0px !important; +} + +::ng-deep.mat-menu-panel { + /* Override mat-menu-panel's default CSS. */ + width: 280px; + max-width: none !important; +} + .popup-container { - padding-top: 0px; - padding-left: 15px; - padding-right: 15px; flex-direction: column; } @@ -10,13 +19,70 @@ } .scroll-container { - height: 400px; + max-height: 400px; width: 100%; overflow-y: auto; + position: relative; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } +/* Ref: https://css-scroll-shadows.vercel.app */ +.scroll-container::before { + pointer-events: none; + content: ''; + position: absolute; + z-index: 2; + height: 7px; + width: 100%; + display: block; + background-image: linear-gradient(to bottom, white 66%, transparent); +} + +.scroll-container::after { + pointer-events: none; + content: ''; + position: sticky; + z-index: 2; + top: 100%; + height: 7px; + width: 100%; + display: block; + background-image: linear-gradient(to top, white 66%, transparent); +} + +.scroll-container-wrapper { + position: relative; +} + +.scroll-container-wrapper::before { + pointer-events: none; + content: ''; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + height: 5px; + background-image: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.7), transparent); +} + +.scroll-container-wrapper::after { + pointer-events: none; + content: ''; + position: absolute; + z-index: 1; + bottom: 0; + left: 0; + right: 0; + height: 5px; + background-image: radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.7), transparent); +} + +.scroll-container::-webkit-scrollbar { + display: none; /* Hide scrollbar for Chrome, Safari and Opera */ +} + .flexbox-container { display: flex; flex-direction: row; @@ -24,12 +90,9 @@ align-items: center; } -.scroll-container::-webkit-scrollbar { - display: none; /* Hide scrollbar for Chrome, Safari and Opera */ -} - .input-field { - width: 100%; + width: calc(100% - (2 * 15px)); /* To account for left and right padding. */ + padding: 0 15px; } .list-option { @@ -37,11 +100,14 @@ } .mat-chip { + height: auto; + padding: 5.5px 7px; + line-height: 1.1em; position: inherit; border-radius: 6px; font-size: 12px; - padding: 11px 7px; min-height: 16px; + max-height: 42px; margin: 0px; top: 50%; } @@ -57,3 +123,9 @@ mat-list-option { width: max-content; } + +.no-labels { + /* Chosen to look similar to button above. */ + padding: 0 16px; + font-size: 14px; +} diff --git a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html index 6a865452e..5a958a311 100644 --- a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html +++ b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html @@ -13,34 +13,38 @@ -
- - -
- - + - - {{ label.name }} - -
-
-
+
No Labels Found!
+
+
+ + +
+ + + + {{ label.name }} + +
+
+
+
diff --git a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts index ecb0ce8e1..9ce86280e 100644 --- a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts +++ b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts @@ -1,7 +1,7 @@ -import { AfterViewInit, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { MatListOption } from '@angular/material/list'; +import { AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MatListOption, MatSelectionList } from '@angular/material/list'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { SimplifiedLabel } from '../../../core/models/label.model'; +import { SimpleLabel } from '../../../core/models/label.model'; import { LabelService } from '../../../core/services/label.service'; import { LoggingService } from '../../../core/services/logging.service'; @@ -13,8 +13,10 @@ import { LoggingService } from '../../../core/services/logging.service'; export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy { @Input() selectedLabels: BehaviorSubject; @Input() hiddenLabels: BehaviorSubject>; + @ViewChild(MatSelectionList) matSelectionList; - labels$: Observable; + labels$: Observable; + allLabels: SimpleLabel[]; selectedLabelNames: string[] = []; hiddenLabelNames: Set = new Set(); loaded = false; @@ -31,6 +33,9 @@ export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy setTimeout(() => { this.load(); this.labels$ = this.labelService.connect(); + this.labels$.subscribe((labels) => { + this.allLabels = labels; + }); }); } @@ -86,4 +91,20 @@ export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy filter(filter: string, target: string): boolean { return !target.toLowerCase().includes(filter.toLowerCase()); } + + hasLabels(filter: string): boolean { + if (this.allLabels === undefined || this.allLabels.length === 0) { + return false; + } + return this.allLabels.some((label) => !this.filter(filter, label.name)); + } + + updateSelection(): void { + this.selectedLabels.next(this.selectedLabelNames); + } + + removeAllSelection(): void { + this.matSelectionList.deselectAll(); + this.updateSelection(); + } } diff --git a/src/app/shared/issue-tables/IssuesDataTable.ts b/src/app/shared/issue-tables/IssuesDataTable.ts index 03ba121ef..23cfbdf7b 100644 --- a/src/app/shared/issue-tables/IssuesDataTable.ts +++ b/src/app/shared/issue-tables/IssuesDataTable.ts @@ -58,14 +58,9 @@ export class IssuesDataTable extends DataSource implements FilterableSour page = this.paginator.page; } - const displayDataChanges = [ - this.issueService.issues$, - page, - sortChange, - this.filterChange, - this.teamFilterChange, - this.dropdownFilterChange - ].filter((x) => x !== undefined); + const displayDataChanges = [this.issueService.issues$, page, sortChange, this.filterChange, this.dropdownFilterChange].filter( + (x) => x !== undefined + ); this.issueService.startPollIssues(); this.issueSubscription = merge(...displayDataChanges) @@ -98,7 +93,6 @@ export class IssuesDataTable extends DataSource implements FilterableSour if (this.sort !== undefined) { data = getSortedData(this.sort, data); } - data = this.getFilteredTeamData(data); data = applySearchFilter(this.filter, this.displayedColumn, this.issueService, data); this.count = data.length; @@ -121,24 +115,6 @@ export class IssuesDataTable extends DataSource implements FilterableSour this.filterChange.next(filter); } - get teamFilter(): string { - return this.teamFilterChange.value; - } - - set teamFilter(teamFilter: string) { - this.teamFilterChange.next(teamFilter); - this.issueService.setIssueTeamFilter(this.teamFilterChange.value); - } - - private getFilteredTeamData(data: Issue[]): Issue[] { - return data.filter((issue) => { - if (!this.teamFilter || this.teamFilter === 'All Teams') { - return true; - } - return issue.teamAssigned.id === this.teamFilter; - }); - } - get dropdownFilter(): DropdownFilter { return this.dropdownFilterChange.value; } diff --git a/src/app/shared/issue-tables/issue-sorter.ts b/src/app/shared/issue-tables/issue-sorter.ts index 78e997c2b..f9477d2c1 100644 --- a/src/app/shared/issue-tables/issue-sorter.ts +++ b/src/app/shared/issue-tables/issue-sorter.ts @@ -1,6 +1,6 @@ import { MatSort } from '@angular/material/sort'; import * as moment from 'moment'; -import { Issue, ISSUE_TYPE_ORDER, SEVERITY_ORDER } from '../../core/models/issue.model'; +import { Issue } from '../../core/models/issue.model'; export function getSortedData(sort: MatSort, data: Issue[]): Issue[] { if (!sort.active) { @@ -11,16 +11,8 @@ export function getSortedData(sort: MatSort, data: Issue[]): Issue[] { return data.sort((a, b) => { switch (sort.active) { - case 'type': - return direction * compareByIssueType(a.type, b.type); - case 'severity': - return direction * compareBySeverity(a.severity, b.severity); case 'assignees': return direction * compareByStringValue(a.assignees.join(', '), b.assignees.join(', ')); - case 'teamAssigned': - return direction * compareByStringValue(a.teamAssigned.id, b.teamAssigned.id); - case 'Todo Remaining': - return -direction * compareByIntegerValue(a.numOfUnresolvedDisputes(), b.numOfUnresolvedDisputes()); case 'id': return direction * compareByIntegerValue(a.id, b.id); case 'date': @@ -32,20 +24,6 @@ export function getSortedData(sort: MatSort, data: Issue[]): Issue[] { }); } -function compareBySeverity(severityA: string, severityB: string): number { - const orderA = SEVERITY_ORDER[severityA]; - const orderB = SEVERITY_ORDER[severityB]; - - return compareByIntegerValue(orderA, orderB); -} - -function compareByIssueType(issueTypeA: string, issueTypeB: string): number { - const orderA = ISSUE_TYPE_ORDER[issueTypeA]; - const orderB = ISSUE_TYPE_ORDER[issueTypeB]; - - return compareByIntegerValue(orderA, orderB); -} - function compareByStringValue(valueA: string, valueB: string): number { const orderA = String(valueA || '').toUpperCase(); const orderB = String(valueB || '').toUpperCase(); diff --git a/src/app/shared/layout/header.component.ts b/src/app/shared/layout/header.component.ts index a03748086..4a965fb05 100644 --- a/src/app/shared/layout/header.component.ts +++ b/src/app/shared/layout/header.component.ts @@ -207,12 +207,13 @@ export class HeaderComponent implements OnInit { * Change repository viewed on Issue Dashboard, if a valid repository is provided. */ changeRepositoryIfValid(repo: Repo, newRepoString: string) { - this.phaseService.changeRepositoryIfValid(repo) - .then(() => { - this.auth.setTitleWithPhaseDetail(); - this.currentRepo = newRepoString; - }) - .catch((error) => this.errorHandlingService.handleError(error)); + this.phaseService + .changeRepositoryIfValid(repo) + .then(() => { + this.auth.setTitleWithPhaseDetail(); + this.currentRepo = newRepoString; + }) + .catch((error) => this.errorHandlingService.handleError(error)); } openChangeRepoDialog() { diff --git a/src/app/shared/lib/marked.ts b/src/app/shared/lib/marked.ts deleted file mode 100644 index 7b9a70024..000000000 --- a/src/app/shared/lib/marked.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; - -export function markedOptionsFactory(): MarkedOptions { - const renderer = new MarkedRenderer(); - const linkRenderer = renderer.link; - - renderer.link = (href, title, text) => { - const html = linkRenderer.call(renderer, href, title, text); - return html.replace(/^Change repository
- - Repository Location (Org/Repo) - - - - {{suggestion}} - - - + + + Repository Location (Org/Repo) + + + + {{ suggestion }} + + + +
diff --git a/src/app/shared/repo-change-form/repo-change-form.component.ts b/src/app/shared/repo-change-form/repo-change-form.component.ts index 86580870e..14db27458 100644 --- a/src/app/shared/repo-change-form/repo-change-form.component.ts +++ b/src/app/shared/repo-change-form/repo-change-form.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { RepoUrlCacheService } from '../../core/services/repo-url-cache.service'; @Component({ selector: 'app-repo-change-form', @@ -11,11 +11,14 @@ import { map, startWith } from 'rxjs/operators'; }) export class RepoChangeFormComponent implements OnInit { public repoName: String; - suggestions: string[]; filteredSuggestions: Observable; repoChangeForm = new FormControl(); - constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data) { + constructor( + public dialogRef: MatDialogRef, + private repoUrlCacheService: RepoUrlCacheService, + @Inject(MAT_DIALOG_DATA) public data + ) { this.repoName = data.repoName; } @@ -24,14 +27,9 @@ export class RepoChangeFormComponent implements OnInit { } private initRepoChangeForm() { - this.suggestions = JSON.parse(window.localStorage.getItem('suggestions')) || []; - // Ref: https://v10.material.angular.io/components/autocomplete/overview - this.filteredSuggestions = this.repoChangeForm.valueChanges - .pipe( - startWith(''), - map(value => this.suggestions.filter(suggestion => suggestion.toLowerCase().includes(value.toLowerCase()))) - ); + this.filteredSuggestions = this.repoUrlCacheService.getFilteredSuggestions(this.repoChangeForm); } + onYesClick(): void { this.dialogRef.close(this.repoName); } diff --git a/src/markdown.css b/src/markdown.css deleted file mode 100644 index a4454ff01..000000000 --- a/src/markdown.css +++ /dev/null @@ -1,696 +0,0 @@ -@font-face { - font-family: octicons-link; - src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) - format('woff'); -} - -markdown { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - line-height: 1.5; - color: #24292e; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol'; - font-size: 16px; - word-wrap: break-word; -} - -markdown .pl-c { - color: #6a737d; -} - -markdown .pl-c1, -markdown .pl-s .pl-v { - color: #005cc5; -} - -markdown .pl-e, -markdown .pl-en { - color: #6f42c1; -} - -markdown .pl-smi, -markdown .pl-s .pl-s1 { - color: #24292e; -} - -markdown .pl-ent { - color: #22863a; -} - -markdown .pl-k { - color: #d73a49; -} - -markdown .pl-s, -markdown .pl-pds, -markdown .pl-s .pl-pse .pl-s1, -markdown .pl-sr, -markdown .pl-sr .pl-cce, -markdown .pl-sr .pl-sre, -markdown .pl-sr .pl-sra { - color: #032f62; -} - -markdown .pl-v, -markdown .pl-smw { - color: #e36209; -} - -markdown .pl-bu { - color: #b31d28; -} - -markdown .pl-ii { - color: #fafbfc; - background-color: #b31d28; -} - -markdown .pl-c2 { - color: #fafbfc; - background-color: #d73a49; -} - -markdown .pl-c2::before { - content: '^M'; -} - -markdown .pl-sr .pl-cce { - font-weight: bold; - color: #22863a; -} - -markdown .pl-ml { - color: #735c0f; -} - -markdown .pl-mh, -markdown .pl-mh .pl-en, -markdown .pl-ms { - font-weight: bold; - color: #005cc5; -} - -markdown .pl-mi { - font-style: italic; - color: #24292e; -} - -markdown .pl-mb { - font-weight: bold; - color: #24292e; -} - -markdown .pl-md { - color: #b31d28; - background-color: #ffeef0; -} - -markdown .pl-mi1 { - color: #22863a; - background-color: #f0fff4; -} - -markdown .pl-mc { - color: #e36209; - background-color: #ffebda; -} - -markdown .pl-mi2 { - color: #f6f8fa; - background-color: #005cc5; -} - -markdown .pl-mdr { - font-weight: bold; - color: #6f42c1; -} - -markdown .pl-ba { - color: #586069; -} - -markdown .pl-sg { - color: #959da5; -} - -markdown .pl-corl { - text-decoration: underline; - color: #032f62; -} - -markdown .octicon { - display: inline-block; - vertical-align: text-top; - fill: currentColor; -} - -markdown a { - background-color: transparent; -} - -markdown a:active, -markdown a:hover { - outline-width: 0; -} - -markdown strong { - font-weight: inherit; -} - -markdown strong { - font-weight: bolder; -} - -markdown h1 { - font-size: 2em; - margin: 0.67em 0; -} - -markdown img { - border-style: none; -} - -markdown code, -markdown kbd, -markdown pre { - font-family: monospace, monospace; - font-size: 1em; -} - -markdown hr { - box-sizing: content-box; - height: 0; - overflow: visible; -} - -markdown input { - font: inherit; - margin: 0; -} - -markdown input { - overflow: visible; -} - -markdown [type='checkbox'] { - box-sizing: border-box; - padding: 0; -} - -markdown * { - box-sizing: border-box; -} - -markdown input { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -markdown a { - color: #0366d6; - text-decoration: none; -} - -markdown a:hover { - text-decoration: underline; -} - -markdown strong { - font-weight: 600; -} - -markdown hr { - height: 0; - margin: 15px 0; - overflow: hidden; - background: transparent; - border: 0; - border-bottom: 1px solid #dfe2e5; -} - -markdown hr::before { - display: table; - content: ''; -} - -markdown hr::after { - display: table; - clear: both; - content: ''; -} - -markdown table { - border-spacing: 0; - border-collapse: collapse; -} - -markdown td, -markdown th { - padding: 0; -} - -markdown h1, -markdown h2, -markdown h3, -markdown h4, -markdown h5, -markdown h6 { - margin-top: 0; - margin-bottom: 0; -} - -markdown h1 { - font-size: 32px; - font-weight: 600; -} - -markdown h2 { - font-size: 24px; - font-weight: 600; -} - -markdown h3 { - font-size: 20px; - font-weight: 600; -} - -markdown h4 { - font-size: 16px; - font-weight: 600; -} - -markdown h5 { - font-size: 14px; - font-weight: 600; -} - -markdown h6 { - font-size: 12px; - font-weight: 600; -} - -markdown p { - margin-top: 0; - margin-bottom: 10px; -} - -markdown blockquote { - margin: 0; -} - -markdown ul, -markdown ol { - padding-left: 0; - margin-top: 0; - margin-bottom: 0; -} - -markdown ol ol, -markdown ul ol { - list-style-type: lower-roman; -} - -markdown ul ul ol, -markdown ul ol ol, -markdown ol ul ol, -markdown ol ol ol { - list-style-type: lower-alpha; -} - -markdown dd { - margin-left: 0; -} - -markdown code { - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 12px; -} - -markdown pre { - margin-top: 0; - margin-bottom: 0; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 12px; -} - -markdown .octicon { - vertical-align: text-bottom; -} - -markdown .pl-0 { - padding-left: 0 !important; -} - -markdown .pl-1 { - padding-left: 4px !important; -} - -markdown .pl-2 { - padding-left: 8px !important; -} - -markdown .pl-3 { - padding-left: 16px !important; -} - -markdown .pl-4 { - padding-left: 24px !important; -} - -markdown .pl-5 { - padding-left: 32px !important; -} - -markdown .pl-6 { - padding-left: 40px !important; -} - -markdown::before { - display: table; - content: ''; -} - -markdown::after { - display: table; - clear: both; - content: ''; -} - -markdown > *:first-child { - margin-top: 0 !important; -} - -markdown > *:last-child { - margin-bottom: 0 !important; -} - -markdown a:not([href]) { - color: inherit; - text-decoration: none; -} - -markdown .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -markdown .anchor:focus { - outline: none; -} - -markdown p, -markdown blockquote, -markdown ul, -markdown ol, -markdown dl, -markdown table, -markdown pre { - margin-top: 0; - margin-bottom: 16px; -} - -markdown hr { - height: 0.25em; - padding: 0; - margin: 24px 0; - background-color: #e1e4e8; - border: 0; -} - -markdown blockquote { - padding: 0 1em; - color: #6a737d; - border-left: 0.25em solid #dfe2e5; -} - -markdown blockquote > :first-child { - margin-top: 0; -} - -markdown blockquote > :last-child { - margin-bottom: 0; -} - -markdown kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #444d56; - vertical-align: middle; - background-color: #fafbfc; - border: solid 1px #c6cbd1; - border-bottom-color: #959da5; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #959da5; -} - -markdown h1, -markdown h2, -markdown h3, -markdown h4, -markdown h5, -markdown h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -markdown h1 .octicon-link, -markdown h2 .octicon-link, -markdown h3 .octicon-link, -markdown h4 .octicon-link, -markdown h5 .octicon-link, -markdown h6 .octicon-link { - color: #1b1f23; - vertical-align: middle; - visibility: hidden; -} - -markdown h1:hover .anchor, -markdown h2:hover .anchor, -markdown h3:hover .anchor, -markdown h4:hover .anchor, -markdown h5:hover .anchor, -markdown h6:hover .anchor { - text-decoration: none; -} - -markdown h1:hover .anchor .octicon-link, -markdown h2:hover .anchor .octicon-link, -markdown h3:hover .anchor .octicon-link, -markdown h4:hover .anchor .octicon-link, -markdown h5:hover .anchor .octicon-link, -markdown h6:hover .anchor .octicon-link { - visibility: visible; -} - -markdown h1 { - padding-bottom: 0.3em; - font-size: 2em; - border-bottom: 1px solid #eaecef; -} - -markdown h2 { - padding-bottom: 0.3em; - font-size: 1.5em; - border-bottom: 1px solid #eaecef; -} - -markdown h3 { - font-size: 1.25em; -} - -markdown h4 { - font-size: 1em; -} - -markdown h5 { - font-size: 0.875em; -} - -markdown h6 { - font-size: 0.85em; - color: #6a737d; -} - -markdown ul, -markdown ol { - padding-left: 2em; -} - -markdown ul ul, -markdown ul ol, -markdown ol ol, -markdown ol ul { - margin-top: 0; - margin-bottom: 0; -} - -markdown li { - word-wrap: break-all; -} - -markdown li > p { - margin-top: 16px; -} - -markdown li + li { - margin-top: 0.25em; -} - -markdown dl { - padding: 0; -} - -markdown dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: 600; -} - -markdown dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -markdown table { - display: block; - width: 100%; - overflow: auto; -} - -markdown table th { - font-weight: 600; -} - -markdown table th, -markdown table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; -} - -markdown table tr { - background-color: #fff; - border-top: 1px solid #c6cbd1; -} - -markdown table tr:nth-child(2n) { - background-color: #f6f8fa; -} - -markdown img { - max-width: 100%; - box-sizing: content-box; - background-color: #fff; -} - -markdown img[align='right'] { - padding-left: 20px; -} - -markdown img[align='left'] { - padding-right: 20px; -} - -markdown code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: rgba(27, 31, 35, 0.05); - border-radius: 3px; -} - -markdown pre { - word-wrap: normal; -} - -markdown pre > code { - padding: 0; - margin: 0; - font-size: 100%; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -markdown .highlight { - margin-bottom: 16px; -} - -markdown .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -markdown .highlight pre, -markdown pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border-radius: 3px; -} - -markdown pre code { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -markdown .full-commit .btn-outline:not(:disabled):hover { - color: #005cc5; - border-color: #005cc5; -} - -markdown kbd { - display: inline-block; - padding: 3px 5px; - font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; - line-height: 10px; - color: #444d56; - vertical-align: middle; - background-color: #fafbfc; - border: solid 1px #d1d5da; - border-bottom-color: #c6cbd1; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #c6cbd1; -} - -markdown :checked + .radio-label { - position: relative; - z-index: 1; - border-color: #0366d6; -} - -markdown .task-list-item { - list-style-type: none; -} - -markdown .task-list-item + .task-list-item { - margin-top: 3px; -} - -markdown .task-list-item input { - margin: 0 0.2em 0.25em -1.6em; - vertical-align: middle; -} - -markdown hr { - border-bottom-color: #eee; -} diff --git a/src/typings.d.ts b/src/typings.d.ts index 78708ff3e..6f504bd8c 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -4,7 +4,7 @@ interface NodeModule { id: string; } -declare var window: Window; +declare var window: Window & typeof globalThis; interface Window { process: any; require: any; diff --git a/tests/app/shared/lib/marked.spec.ts b/tests/app/shared/lib/marked.spec.ts deleted file mode 100644 index 448bf32a3..000000000 --- a/tests/app/shared/lib/marked.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; -import { markedOptionsFactory } from '../../../../src/app/shared/lib/marked'; - -describe('markedOptionsFactory', () => { - const markedOptions: MarkedOptions = markedOptionsFactory(); - const renderer: MarkedRenderer = markedOptions.renderer; - - const CUSTOM_LINK = 'www.google.com'; - const CUSTOM_TITLE = 'google'; - const CUSTOM_TEXT = 'link here'; - - it('should append all links with target=_blank', () => { - const htmlOutput: string = renderer.link(CUSTOM_LINK, CUSTOM_TITLE, CUSTOM_TEXT); - expect(htmlOutput).toContain('target="_blank"'); - }); -}); diff --git a/tests/model/checkbox.model.spec.ts b/tests/model/checkbox.model.spec.ts deleted file mode 100644 index 1b8339394..000000000 --- a/tests/model/checkbox.model.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Checkbox } from '../../src/app/core/models/checkbox.model'; - -const EXPECTED_CHECKED_STRING = '- [x] todo'; -const EXPECTED_UNCHECKED_STRING = '- [ ] todo'; - -describe('Checkbox', () => { - it('.setChecked() sets the correct isChecked value', () => { - const checkbox = new Checkbox('todo', false); - checkbox.setChecked(true); - expect(checkbox.isChecked).toBe(true); - }); - - it('formats the correct toString value', () => { - const falseCheckbox = new Checkbox('todo', false); - const trueCheckbox = new Checkbox('todo', true); - - expect(falseCheckbox.toString()).toBe(EXPECTED_UNCHECKED_STRING); - expect(trueCheckbox.toString()).toBe(EXPECTED_CHECKED_STRING); - }); -}); diff --git a/tests/model/templates/sections/issue-dispute-section-parser.model.spec.ts b/tests/model/templates/sections/issue-dispute-section-parser.model.spec.ts deleted file mode 100644 index c137ab3aa..000000000 --- a/tests/model/templates/sections/issue-dispute-section-parser.model.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IssueDisputeSectionParser } from '../../../../src/app/core/models/templates/sections/issue-dispute-section-parser.model'; - -const TYPE_DISPUTE = - '## :question: Issue Type\n\n' + - '### Team says:\n\n' + - "{the team's action that is being disputed}\n\n" + - '### Tester says:\n\n' + - "{tester's objection}\n\n" + - '-------------------'; - -const EXPECTED_TITLE = 'Issue Type'; -const EXPECTED_DESCRIPTION = - '### Team says:\n\n' + "{the team's action that is being disputed}\n\n" + '### Tester says:\n\n' + "{tester's objection}"; - -describe('IssueDisputeSectionParser', () => { - it('parses type dispute correctly', () => { - const result = IssueDisputeSectionParser.run(TYPE_DISPUTE).result; - - expect(result.title).toBe(EXPECTED_TITLE); - expect(result.description).toBe(EXPECTED_DESCRIPTION); - }); -}); diff --git a/tests/model/templates/sections/moderation-section-parser.model.spec.ts b/tests/model/templates/sections/moderation-section-parser.model.spec.ts deleted file mode 100644 index 1e03e2243..000000000 --- a/tests/model/templates/sections/moderation-section-parser.model.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - DoneCheckboxParser, - ModerationSectionParser -} from '../../../../src/app/core/models/templates/sections/moderation-section-parser.model'; - -const TYPE_DISPUTE = '## :question: Issue Type\n\n' + '- [ ] Done\n\n' + '[replace this with your explanation]\n\n' + '-------------------'; - -const EMPTY_DONE_CHECKBOX = '- [ ] Done'; -const FILLED_DONE_CHECKBOX = '- [x] Done'; - -const EXPECTED_TITLE = 'Issue Type'; -const EXPECTED_DESCRIPTION = '- [ ] Done\n\n[replace this with your explanation]'; -const EXPECTED_TUTOR_RESPONSE = '[replace this with your explanation]'; - -describe('DoneCheckboxParser', () => { - it('parses empty checkbox correctly', () => { - const result = DoneCheckboxParser.run(EMPTY_DONE_CHECKBOX).result; - - expect(result).toBe(false); - }); - it('parses filled checkbox correctly', () => { - const result = DoneCheckboxParser.run(FILLED_DONE_CHECKBOX).result; - - expect(result).toBe(true); - }); -}); - -describe('ModerationSectionParser', () => { - it('parses type dispute correctly', () => { - const result = ModerationSectionParser.run(TYPE_DISPUTE).result; - - expect(result.title).toBe(EXPECTED_TITLE); - expect(result.description).toBe(EXPECTED_DESCRIPTION); - expect(result.todo.isChecked).toBe(false); - expect(result.tutorResponse).toBe(EXPECTED_TUTOR_RESPONSE); - }); -}); diff --git a/tests/model/templates/sections/tester-response-section-parser.model.spec.ts b/tests/model/templates/sections/tester-response-section-parser.model.spec.ts deleted file mode 100644 index a9e0e9a23..000000000 --- a/tests/model/templates/sections/tester-response-section-parser.model.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - DisagreeCheckboxParser, - TesterResponseSectionParser -} from '../../../../src/app/core/models/templates/sections/tester-response-section-parser.model'; - -const EMPTY_DISAGREE_CHECKBOX = '- [ ] I disagree'; -const FILLED_DISAGREE_CHECKBOX = '- [x] I disagree'; -const DEFAULT_DISAGREEMENT_REASON = '[replace this with your reason]'; -const USER_DISAGREEMENT_REASON = 'I disagree!'; - -const RESPONSE_TITLE = 'response'; -const RESPONSE_DESCRIPTION = 'Team chose [`response.Rejected`]'; -const RESPONSE_TEAM_CHOSE = 'Rejected'; - -const SEVERITY_TITLE = 'severity'; -const SEVERITY_DESCRIPTION = 'Team chose [`severity.Low`]\nOriginally [`severity.High`]'; -const SEVERITY_TEAM_CHOSE = 'Low'; -const SEVERITY_TESTER_CHOSE = 'High'; - -const TYPE_TITLE = 'type'; -const TYPE_DESCRIPTION = 'Team chose [`type.DocumentationBug`]\nOriginally [`type.FunctionalityBug`]'; -const TYPE_TEAM_CHOSE = 'DocumentationBug'; -const TYPE_TESTER_CHOSE = 'FunctionalityBug'; - -const DUPLICATE_TITLE = 'duplicate status'; -const DUPLICATE_DESCRIPTION = - "Team chose to mark this issue as a duplicate of another issue (as explained in the _**Team's response**_ above)"; - -export const RESPONSE_DISAGREEMENT = - '## :question: Issue response\n\n' + - 'Team chose [`response.Rejected`]\n\n' + - '- [ ] I disagree\n\n' + - '**Reason for disagreement:** [replace this with your reason]\n\n' + - '-------------------'; - -export const SEVERITY_DISAGREEMENT = - '## :question: Issue severity\n\n' + - 'Team chose [`severity.Low`]\n' + - 'Originally [`severity.High`]\n\n' + - '- [x] I disagree\n\n' + - '**Reason for disagreement:** I disagree!\n\n' + - '-------------------'; - -export const TYPE_DISAGREEMENT = - '## :question: Issue type\n\n' + - 'Team chose [`type.DocumentationBug`]\n' + - 'Originally [`type.FunctionalityBug`]\n\n' + - '- [ ] I disagree\n\n' + - '**Reason for disagreement:** [replace this with your reason]\n\n' + - '-------------------'; - -export const DUPLICATE_DISAGREEMENT = - '## :question: Issue duplicate status\n\n' + - "Team chose to mark this issue as a duplicate of another issue (as explained in the _**Team's response**_ above)\n\n" + - '- [ ] I disagree\n\n' + - '**Reason for disagreement:** [replace this with your reason]\n\n' + - '-------------------'; - -describe('DisagreeCheckboxParser', () => { - it('parses empty checkbox correctly', () => { - const result = DisagreeCheckboxParser.run(EMPTY_DISAGREE_CHECKBOX).result; - - expect(result).toBe(false); - }); - it('parses filled checkbox correctly', () => { - const result = DisagreeCheckboxParser.run(FILLED_DISAGREE_CHECKBOX).result; - - expect(result).toBe(true); - }); -}); - -describe('TesterResponseSectionParser', () => { - it('parses response disagreement correctly', () => { - const result = TesterResponseSectionParser.run(RESPONSE_DISAGREEMENT).result; - - expect(result.title).toBe(RESPONSE_TITLE); - expect(result.description).toBe(RESPONSE_DESCRIPTION); - expect(result.teamChose).toBe(RESPONSE_TEAM_CHOSE); - expect(result.testerChose).toBe(null); - expect(result.disagreeCheckboxValue).toBe(false); - expect(result.reasonForDisagreement).toBe(DEFAULT_DISAGREEMENT_REASON); - }); - it('parses severity disagreement correctly', () => { - const result = TesterResponseSectionParser.run(SEVERITY_DISAGREEMENT).result; - - expect(result.title).toBe(SEVERITY_TITLE); - expect(result.description).toBe(SEVERITY_DESCRIPTION); - expect(result.teamChose).toBe(SEVERITY_TEAM_CHOSE); - expect(result.testerChose).toBe(SEVERITY_TESTER_CHOSE); - expect(result.disagreeCheckboxValue).toBe(true); - expect(result.reasonForDisagreement).toBe(USER_DISAGREEMENT_REASON); - }); - it('parses type disagreement correctly', () => { - const result = TesterResponseSectionParser.run(TYPE_DISAGREEMENT).result; - - expect(result.title).toBe(TYPE_TITLE); - expect(result.description).toBe(TYPE_DESCRIPTION); - expect(result.teamChose).toBe(TYPE_TEAM_CHOSE); - expect(result.testerChose).toBe(TYPE_TESTER_CHOSE); - expect(result.disagreeCheckboxValue).toBe(false); - expect(result.reasonForDisagreement).toBe(DEFAULT_DISAGREEMENT_REASON); - }); - it('parses duplicate status disagreement correctly', () => { - const result = TesterResponseSectionParser.run(DUPLICATE_DISAGREEMENT).result; - - expect(result.title).toBe(DUPLICATE_TITLE); - expect(result.description).toBe(DUPLICATE_DESCRIPTION); - expect(result.teamChose).toBe(null); - expect(result.testerChose).toBe(null); - expect(result.disagreeCheckboxValue).toBe(false); - expect(result.reasonForDisagreement).toBe(DEFAULT_DISAGREEMENT_REASON); - }); -}); diff --git a/tests/model/templates/team-accepted-template.model.spec.ts b/tests/model/templates/team-accepted-template.model.spec.ts deleted file mode 100644 index d87f08aad..000000000 --- a/tests/model/templates/team-accepted-template.model.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GithubComment } from '../../../src/app/core/models/github/github-comment.model'; -import { TeamAcceptedMessage, TeamAcceptedTemplate } from '../../../src/app/core/models/templates/team-accepted-template.model'; - -import { TEAM_RESPONSE_MULTIPLE_DISAGREEMENT } from '../../constants/githubcomment.constants'; - -const EMPTY_BODY_GITHUB_COMMENT = { - body: '' -} as GithubComment; - -const ACCEPTED_MESSAGE_GITHUB_COMMENT = { - body: TeamAcceptedMessage -} as GithubComment; - -const hasAcceptedComment = [EMPTY_BODY_GITHUB_COMMENT, ACCEPTED_MESSAGE_GITHUB_COMMENT]; -const noAcceptedComment = [EMPTY_BODY_GITHUB_COMMENT, TEAM_RESPONSE_MULTIPLE_DISAGREEMENT]; - -describe('TeamAcceptedTemplate class', () => { - it('parses team accepted message correctly', () => { - const template = new TeamAcceptedTemplate([ACCEPTED_MESSAGE_GITHUB_COMMENT]); - - expect(template.teamAccepted).toBe(true); - }); - it('finds team accepted comment correctly', () => { - const template = new TeamAcceptedTemplate(hasAcceptedComment); - - expect(template.teamAccepted).toBe(true); - }); - it('does not find team accepted comment', () => { - const template = new TeamAcceptedTemplate(noAcceptedComment); - - expect(template.teamAccepted).not.toBe(true); - }); -}); diff --git a/tests/model/templates/team-response-template.model.spec.ts b/tests/model/templates/team-response-template.model.spec.ts deleted file mode 100644 index 430b64786..000000000 --- a/tests/model/templates/team-response-template.model.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { GithubComment } from '../../../src/app/core/models/github/github-comment.model'; -import { TeamResponseParser, TeamResponseTemplate } from '../../../src/app/core/models/templates/team-response-template.model'; - -const EMPTY_BODY_GITHUB_COMMENT = { - body: '' -} as GithubComment; -const EXPECTED_TEAM_RESPONSE_HEADER = "# Team's Response"; -const EXPECTED_TEAM_RESPONSE_TEMPLATE_CONTENT = 'This is a simple response'; -const DUPLICATE_ISSUE_NUMBER = 100; - -const TEAM_RESPONSE_WITH_EXTRA_NEWLINES_AND_WHITESPACE = - EXPECTED_TEAM_RESPONSE_HEADER + '\r\n \n ' + EXPECTED_TEAM_RESPONSE_TEMPLATE_CONTENT + '\r\n \n\n ' + '## Duplicate status (if any): --'; -const TEAM_RESPONSE_WITH_DUPLICATE = - EXPECTED_TEAM_RESPONSE_HEADER + - '\r\n' + - EXPECTED_TEAM_RESPONSE_TEMPLATE_CONTENT + - '\r\n' + - '## Duplicate status (if any): Duplicate of #' + - DUPLICATE_ISSUE_NUMBER; - -describe('TeamResponseParser', () => { - it('parses the team response correctly', () => { - const result = TeamResponseParser.run(TEAM_RESPONSE_WITH_EXTRA_NEWLINES_AND_WHITESPACE).result; - - expect(result.teamResponse).toBe(EXPECTED_TEAM_RESPONSE_TEMPLATE_CONTENT); - expect(result.issueNumber).toBe(null); - }); - it('parses the duplicate issue number correctly', () => { - const result = TeamResponseParser.run(TEAM_RESPONSE_WITH_DUPLICATE).result; - - expect(result.issueNumber).toBe(DUPLICATE_ISSUE_NUMBER); - }); -}); - -describe('TeamResponseTemplate', () => { - it('parses the teamResponse correctly', () => { - EMPTY_BODY_GITHUB_COMMENT.body = TEAM_RESPONSE_WITH_EXTRA_NEWLINES_AND_WHITESPACE; - const template = new TeamResponseTemplate([EMPTY_BODY_GITHUB_COMMENT]); - - expect(template.teamResponse.content).toBe(EXPECTED_TEAM_RESPONSE_TEMPLATE_CONTENT); - expect(template.teamResponse.header.toString()).toBe(EXPECTED_TEAM_RESPONSE_HEADER); - expect(template.duplicateOf.issueNumber).toEqual(null); - }); - it('parses the duplicateOf value correctly', () => { - EMPTY_BODY_GITHUB_COMMENT.body = TEAM_RESPONSE_WITH_DUPLICATE; - const template = new TeamResponseTemplate([EMPTY_BODY_GITHUB_COMMENT]); - - expect(template.duplicateOf.issueNumber).toBe(DUPLICATE_ISSUE_NUMBER); - }); -}); diff --git a/tests/model/templates/tester-response-template.model.spec.ts b/tests/model/templates/tester-response-template.model.spec.ts deleted file mode 100644 index 3957746d7..000000000 --- a/tests/model/templates/tester-response-template.model.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { TesterResponseParser, TesterResponseTemplate } from '../../../src/app/core/models/templates/tester-response-template.model'; - -import { TEAM_RESPONSE_MULTIPLE_DISAGREEMENT } from '../../constants/githubcomment.constants'; - -const SEVERITY_LOW = 'Low'; -const TYPE_DOCUMENTATION_BUG = 'DocumentationBug'; - -const EXPECTED_TEAM_RESPONSE_CONTENT = 'This is a dummy team response comment: Thanks for the feedback'; -const EXPECTED_TEAM_RESPONSE_HEADER = "# Team's Response"; - -const ISSUE_SEVERITY_TITLE = 'Issue severity'; -const ISSUE_TYPE_TITLE = 'Issue type'; -const DISAGREE_CHECKBOX = '- [ ] I disagree'; -const DISAGREE_REASON = '[replace this with your reason]'; - -const ISSUE_SEVERITY_DESCRIPTION = 'Team chose [`severity.Low`]\nOriginally [`severity.High`]'; -const ISSUE_TYPE_DESCRIPTION = 'Team chose [`type.DocumentationBug`]\nOriginally [`type.FunctionalityBug`]'; - -describe('TesterResponseParser', () => { - describe('testerDisagree, teamChosenSeverity and teamChosenType fields', () => { - it('fields are parsed correctly from the body of the GithubComment', () => { - const result = TesterResponseParser.run(TEAM_RESPONSE_MULTIPLE_DISAGREEMENT.body).result; - - expect(result.testerDisagree).toBe(false); - expect(result.teamChosenSeverity).toBe(SEVERITY_LOW); - expect(result.teamChosenType).toBe(TYPE_DOCUMENTATION_BUG); - }); - }); - describe('testerResponse and teamResponse fields', () => { - it('parses the fields correctly from the body of the GithubComment', () => { - const result = TesterResponseParser.run(TEAM_RESPONSE_MULTIPLE_DISAGREEMENT.body).result; - - expect(result.teamResponse).toBe(EXPECTED_TEAM_RESPONSE_CONTENT); - - expect(result.testerResponses[0].title).toBe(ISSUE_SEVERITY_TITLE); - expect(result.testerResponses[0].description).toBe(ISSUE_SEVERITY_DESCRIPTION); - expect(result.testerResponses[0].disagreeCheckbox.toString()).toBe(DISAGREE_CHECKBOX); - expect(result.testerResponses[0].reasonForDisagreement).toBe(DISAGREE_REASON); - - expect(result.testerResponses[1].title).toBe(ISSUE_TYPE_TITLE); - expect(result.testerResponses[1].description).toBe(ISSUE_TYPE_DESCRIPTION); - expect(result.testerResponses[1].disagreeCheckbox.toString()).toBe(DISAGREE_CHECKBOX); - expect(result.testerResponses[1].reasonForDisagreement).toBe(DISAGREE_REASON); - }); - }); -}); - -describe('TesterResponseTemplate class', () => { - describe('teamChosenType and teamChosenSeverity fields', () => { - it('parses the teamChosenType and teamChosenSeverity values correctly from the GithubComment', () => { - const template = new TesterResponseTemplate([TEAM_RESPONSE_MULTIPLE_DISAGREEMENT]); - - expect(template.teamChosenSeverity).toBe(SEVERITY_LOW); - expect(template.teamChosenType).toBe(TYPE_DOCUMENTATION_BUG); - }); - }); - describe('testerResponse and teamResponse fields', () => { - it('parses the testerResponse and teamResponse fields correctly from the GithubComment', () => { - const template = new TesterResponseTemplate([TEAM_RESPONSE_MULTIPLE_DISAGREEMENT]); - - expect(template.teamResponse.content).toBe(EXPECTED_TEAM_RESPONSE_CONTENT); - expect(template.teamResponse.header.toString()).toBe(EXPECTED_TEAM_RESPONSE_HEADER); - - expect(template.testerResponse.testerResponses[0].title).toBe(ISSUE_SEVERITY_TITLE); - expect(template.testerResponse.testerResponses[0].description).toBe(ISSUE_SEVERITY_DESCRIPTION); - expect(template.testerResponse.testerResponses[0].disagreeCheckbox.toString()).toBe(DISAGREE_CHECKBOX); - expect(template.testerResponse.testerResponses[0].reasonForDisagreement).toBe(DISAGREE_REASON); - - expect(template.testerResponse.testerResponses[1].title).toBe(ISSUE_TYPE_TITLE); - expect(template.testerResponse.testerResponses[1].description).toBe(ISSUE_TYPE_DESCRIPTION); - expect(template.testerResponse.testerResponses[1].disagreeCheckbox.toString()).toBe(DISAGREE_CHECKBOX); - expect(template.testerResponse.testerResponses[1].reasonForDisagreement).toBe(DISAGREE_REASON); - }); - }); -}); diff --git a/tests/model/templates/tutor-moderation-issue-template.model.spec.ts b/tests/model/templates/tutor-moderation-issue-template.model.spec.ts deleted file mode 100644 index f450126a8..000000000 --- a/tests/model/templates/tutor-moderation-issue-template.model.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - TutorModerationIssueParser, - TutorModerationIssueTemplate -} from '../../../src/app/core/models/templates/tutor-moderation-issue-template.model'; - -import { ISSUE_PENDING_MODERATION } from '../../constants/githubissue.constants'; - -const EXPECTED_ISSUE_DESCRIPTION = '{original issue description}'; -const EXPECTED_TEAM_RESPONSE = "{team's response}"; -const EXPECTED_DISPUTE_DESCRIPTION = - "### Team says:\r\n{the team's action that is being disputed}\r\n\r\n" + "### Tester says:\r\n{tester's objection}"; -const TYPE_TITLE = 'Issue Type'; -const SEVERITY_TITLE = 'Issue Severity'; -const NOT_RELATED_TITLE = 'Not Related Question'; - -describe('TutorModerationIssueParser', () => { - it('parses the issue description and team response correctly', () => { - const result = TutorModerationIssueParser.run(ISSUE_PENDING_MODERATION.body).result; - - expect(result.description).toBe(EXPECTED_ISSUE_DESCRIPTION); - expect(result.teamResponse).toBe(EXPECTED_TEAM_RESPONSE); - }); - it('parses the issue disputes correctly', () => { - const result = TutorModerationIssueParser.run(ISSUE_PENDING_MODERATION.body).result; - - expect(result.issueDisputes[0].title).toBe(TYPE_TITLE); - expect(result.issueDisputes[0].description).toBe(EXPECTED_DISPUTE_DESCRIPTION); - - expect(result.issueDisputes[1].title).toBe(SEVERITY_TITLE); - expect(result.issueDisputes[1].description).toBe(EXPECTED_DISPUTE_DESCRIPTION); - - expect(result.issueDisputes[2].title).toBe(NOT_RELATED_TITLE); - expect(result.issueDisputes[2].description).toBe(EXPECTED_DISPUTE_DESCRIPTION); - }); -}); - -describe('TutorModerationIssueTemplate class', () => { - it('parses a tutor moderation issue successfully', () => { - const template = new TutorModerationIssueTemplate(ISSUE_PENDING_MODERATION); - - expect(template.description.content).toBe(EXPECTED_ISSUE_DESCRIPTION); - expect(template.teamResponse.content).toBe(EXPECTED_TEAM_RESPONSE); - }); -}); diff --git a/tests/model/templates/tutor-moderation-todo-template.model.spec.ts b/tests/model/templates/tutor-moderation-todo-template.model.spec.ts deleted file mode 100644 index 67a4766c1..000000000 --- a/tests/model/templates/tutor-moderation-todo-template.model.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - TutorModerationTodoParser, - TutorModerationTodoTemplate -} from '../../../src/app/core/models/templates/tutor-moderation-todo-template.model'; - -import { PENDING_TUTOR_MODERATION } from '../../constants/githubcomment.constants'; - -const EMPTY_DONE_CHECKBOX = '- [ ] Done'; -const FILLED_DONE_CHECKBOX = '- [x] Done'; -const DEFAULT_TUTOR_RESPONSE = '[replace this with your explanation]'; -const TEST_TUTOR_RESPONSE = 'test'; -const DEFAULT_DESCRIPTION = EMPTY_DONE_CHECKBOX + '\n\n' + DEFAULT_TUTOR_RESPONSE; -const TEST_DESCRIPTION = FILLED_DONE_CHECKBOX + '\n\n' + TEST_TUTOR_RESPONSE; - -const TYPE_TITLE = 'Issue Type'; -const SEVERITY_TITLE = 'Issue Severity'; -const NOT_RELATED_TITLE = 'Not Related Question'; - -describe('TutorModerationTodoParser', () => { - it('parses comment body correctly', () => { - const result = TutorModerationTodoParser.run(PENDING_TUTOR_MODERATION.body).result; - - expect(result.disputesToResolve[0].title).toBe(TYPE_TITLE); - expect(result.disputesToResolve[0].description).toBe(TEST_DESCRIPTION); - expect(result.disputesToResolve[0].todo.isChecked).toBe(true); - expect(result.disputesToResolve[0].tutorResponse).toBe(TEST_TUTOR_RESPONSE); - - expect(result.disputesToResolve[1].title).toBe(SEVERITY_TITLE); - expect(result.disputesToResolve[1].description).toBe(DEFAULT_DESCRIPTION); - expect(result.disputesToResolve[1].todo.isChecked).toBe(false); - expect(result.disputesToResolve[1].tutorResponse).toBe(DEFAULT_TUTOR_RESPONSE); - - expect(result.disputesToResolve[2].title).toBe(NOT_RELATED_TITLE); - expect(result.disputesToResolve[2].description).toBe(DEFAULT_DESCRIPTION); - expect(result.disputesToResolve[2].todo.isChecked).toBe(false); - expect(result.disputesToResolve[2].tutorResponse).toBe(DEFAULT_TUTOR_RESPONSE); - }); -}); - -describe('TutorModerationTodoTemplate', () => { - it('parses the github comment successfully', () => { - const template = new TutorModerationTodoTemplate([PENDING_TUTOR_MODERATION]); - - expect(template.parseFailure).not.toBe(true); - }); -}); diff --git a/tests/services/repo-url-cache.service.spec.ts b/tests/services/repo-url-cache.service.spec.ts new file mode 100644 index 000000000..e51a473eb --- /dev/null +++ b/tests/services/repo-url-cache.service.spec.ts @@ -0,0 +1,79 @@ +import { FormControl } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { RepoUrlCacheService } from '../../src/app/core/services/repo-url-cache.service'; +import { MockLocalStorage } from '../helper/mock.local.storage'; + +let repoUrlCacheService: RepoUrlCacheService; + +const repoNameOne = 'mock/repo_one'; +const repoNameTwo = 'mock/repo_two'; + +const mockLocalStorageFunctionCalls = (mockLocalStorage: MockLocalStorage) => { + spyOn(localStorage, 'getItem').and.callFake(mockLocalStorage.getItem.bind(mockLocalStorage)); + spyOn(localStorage, 'setItem').and.callFake(mockLocalStorage.setItem.bind(mockLocalStorage)); + spyOn(localStorage, 'removeItem').and.callFake(mockLocalStorage.removeItem.bind(mockLocalStorage)); + spyOn(localStorage, 'clear').and.callFake(mockLocalStorage.clear.bind(mockLocalStorage)); +}; + +describe('RepoUrlCacheService', () => { + beforeAll(() => { + const mockLocalStorage = new MockLocalStorage(); + mockLocalStorageFunctionCalls(mockLocalStorage); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it('should load with no suggestions if localStorage is empty', () => { + repoUrlCacheService = new RepoUrlCacheService(); + + expect(repoUrlCacheService.suggestions).toEqual([]); + }); + + it('should load with suggestions if localStorage is not empty', () => { + localStorage.setItem(RepoUrlCacheService.KEY_NAME, JSON.stringify([repoNameOne, repoNameTwo])); + + repoUrlCacheService = new RepoUrlCacheService(); + + expect(repoUrlCacheService.suggestions).toEqual([repoNameOne, repoNameTwo]); + }); + + describe('cache()', () => { + it('should update suggestions if it does not already include the repo', () => { + repoUrlCacheService = new RepoUrlCacheService(); + + repoUrlCacheService.cache(repoNameOne); + + // suggestions in repoUrlCacheService should be updated + expect(repoUrlCacheService.suggestions).toEqual([repoNameOne]); + // suggestions in localStorage should be updated + expect(localStorage.getItem(RepoUrlCacheService.KEY_NAME)).toEqual(JSON.stringify([repoNameOne])); + }); + + it('should not update suggestions if it already includes the repo', () => { + localStorage.setItem(RepoUrlCacheService.KEY_NAME, JSON.stringify([repoNameOne])); + + repoUrlCacheService = new RepoUrlCacheService(); + + repoUrlCacheService.cache(repoNameOne); + + // suggestions in repoUrlCacheService should not be updated + expect(repoUrlCacheService.suggestions).toEqual([repoNameOne]); + // suggestions in localStorage should be not updated + expect(localStorage.getItem(RepoUrlCacheService.KEY_NAME)).toEqual(JSON.stringify([repoNameOne])); + }); + }); + + describe('getFilteredSuggestions()', () => { + it('should return an Observable', () => { + const formControl = new FormControl(); + + repoUrlCacheService = new RepoUrlCacheService(); + + const filteredSuggestions = repoUrlCacheService.getFilteredSuggestions(formControl); + + expect(filteredSuggestions).toBeInstanceOf(Observable); + }); + }); +});