Skip to content

Commit

Permalink
Labels Setup (#88)
Browse files Browse the repository at this point in the history
* Labels Setup

Compulsory Labels are now verified and created on login. This goes
together with V2 of the app since the initial repository belongs in each
student's account, now we can ensure that the repo has been properly
setup with the necessary labels.

* Code style fixes

* Label Color Verification On Login

* Refactored Label Data Validation

* Code Refactoring

* Label Interface to Class Morphing

* Lint Fixes

* Codestyle Fixes & Assertion Error Implementation

* Codestyle Fixes

* Further CodeStyle Fixes

* Further CodeStyle Fixes

* Further Codestyle nit Fixes
  • Loading branch information
ptvrajsk authored and RonakLakhotia committed Jun 21, 2019
1 parent 9fd34ec commit 54c91e2
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 51 deletions.
2 changes: 1 addition & 1 deletion src/app/core/models/issue.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export enum RESPONSE {
export const ISSUE_LABELS = {
severity: Object.keys(SEVERITY),
type: Object.keys(TYPE),
responseTag: Object.keys(RESPONSE),
response: Object.keys(RESPONSE),
};

export const IssuesFilter = {
Expand Down
27 changes: 24 additions & 3 deletions src/app/core/models/label.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
export interface Label {
labelValue: string;
labelColor: string;
export class Label {

labelCategory: string;
labelValue: string;
labelColor: string;

constructor(labelCategory: string, labelValue: string, labelColor: string) {
this.labelValue = labelValue;
this.labelColor = labelColor;
this.labelCategory = labelCategory;
}

/**
* Returns the name of the label with the format of
* 'category'.'value' (e.g. severity.Low)
*/
public getFormattedName(): string {
return this.labelCategory + '.' + this.labelValue;
}

public equals(label: Label) {
return this.labelValue === label.labelValue
&& this.labelColor === label.labelColor && this.labelCategory === label.labelCategory;
}
}
2 changes: 1 addition & 1 deletion src/app/core/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class AuthService {
}
}),
flatMap((userResponse) => {
return this.labelService.getAllLabels(userResponse);
return this.labelService.synchronizeRemoteLabels(userResponse);
}),
flatMap((labelResponse) => {
// Initialise last modified time for this repo
Expand Down
18 changes: 18 additions & 0 deletions src/app/core/services/github.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ export class GithubService {
);
}

/**
* Creates a label in the current repository.
* @param formattedLabelName - name of new label.
* @param labelColor - colour of new label.
*/
createLabel(formattedLabelName: string, labelColor: string): void {
octokit.issues.createLabel({owner: ORG_NAME, repo: REPO, name: formattedLabelName, color: labelColor});
}

/**
* Updates a label's information in the current repository.
* @param labelName - name of existing label
* @param labelColor - new color to be assigned to existing label.
*/
updateLabel(labelName: string, labelColor: string): void {
octokit.issues.updateLabel({owner: ORG_NAME, repo: REPO, current_name: labelName, color: labelColor});
}

closeIssue(id: number): Observable<{}> {
return from(octokit.issues.update({owner: ORG_NAME, repo: REPO, number: id, state: 'closed'})).pipe(
map(response => {
Expand Down
147 changes: 101 additions & 46 deletions src/app/core/services/label.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { GithubService } from './github.service';
import { map } from 'rxjs/operators';
import { Label } from '../models/label.model';
import { Observable } from 'rxjs';
import { SEVERITY_ORDER } from '../../core/models/issue.model';
import { User } from '../models/user.model';

/* The threshold to decide if color is dark or light.
Expand All @@ -19,27 +18,50 @@ const COLOR_LIGHT_TEXT = 'FFFFFF'; // Light color for text with dark background
providedIn: 'root'
})
export class LabelService {
private severityLabels: Label[];
private typeLabels: Label[];
private responseLabels: Label[];
private labelColorMap: Map<string, string>;

private readonly REQUIRED_LABELS = {
severity: {
Low: new Label('severity', 'Low', 'ffb3b3'),
Medium: new Label('severity', 'Medium', 'ff6666'),
High: new Label('severity', 'High', 'b30000')
},
type: {
DocumentationBug: new Label('type', 'DocumentationBug', 'ccb3ff'),
FunctionalityBug: new Label('type', 'FunctionalityBug', '661aff')
},
response: {
Accepted: new Label('response', 'Accepted', '80ffcc'),
Rejected: new Label('response', 'Rejected', 'ff80b3'),
IssueUnclear: new Label('response', 'IssueUnclear', 'ffcc80'),
CannotReproduce: new Label('response', 'CannotReproduce', 'bfbfbf')
},
status: {
Done: new Label('status', 'Done', 'b3ecff'),
Incomplete: new Label('status', 'Incomplete', '1ac6ff')
}
};

private severityLabels: Label[] = Object.values(this.REQUIRED_LABELS.severity);
private typeLabels: Label[] = Object.values(this.REQUIRED_LABELS.type);
private responseLabels: Label[] = Object.values(this.REQUIRED_LABELS.response);
private statusLabels: Label[] = Object.values(this.REQUIRED_LABELS.status);
private labelArrays = {
severity: this.severityLabels,
type: this.typeLabels,
response: this.responseLabels,
status: this.statusLabels
};

constructor(private githubService: GithubService) {
this.severityLabels = new Array();
this.typeLabels = new Array();
this.responseLabels = new Array();
this.labelColorMap = new Map();
}

/**
* Calls the Github service api to get all labels from the repository and
* store it in a list of arrays in this label service
* Synchronizes the labels in github with those required by the application.
*/
getAllLabels(userResponse: User): Observable<User> {
synchronizeRemoteLabels(userResponse: User): Observable<User> {
return this.githubService.fetchAllLabels().pipe(
map((response) => {
this.populateLabelLists(response);
this.ensureRepoHasRequiredLabels(this.parseLabelData(response), this.getRequiredLabelsAsArray());
return userResponse;
})
);
Expand All @@ -63,59 +85,92 @@ export class LabelService {
}

/**
* Returns the color of the label using the label-color mapping
* @param labelValue: the label's value (e.g Low / Medium / High)
* @return a string with the color code of the label, or white color if
* no labelValue was provided or no such mapping was found
* Returns the color of the label by searching a list of
* all available labels.
* @param labelValue: the label's value (e.g Low / Medium / High / ...)
*/
getColorOfLabel(labelValue: string): string {
const color = this.labelColorMap.get(labelValue);
// TODO: Rewrite function - labelValue insufficient to differentiate between labels. Should use `labelCategory.labelValue` format.
const WHITE_COLOR = 'ffffff';
if (labelValue === '') {
return WHITE_COLOR;
}

const existingLabel = this.getRequiredLabelsAsArray().find(label => label.labelValue === labelValue);

if (color === undefined || labelValue === '') {
return 'ffffff';
if (existingLabel === undefined || existingLabel.labelColor === undefined) {
return WHITE_COLOR;
} else {
return existingLabel.labelColor;
}
}

private getRequiredLabelsAsArray(): Label[] {
let requiredLabels: Label[] = [];

return color;
for (const category of Object.keys(this.labelArrays)) {
requiredLabels = requiredLabels.concat(this.labelArrays[category]);
}

return requiredLabels;
}

/**
* Stores the json data from Github api into the list of arrays in this service
* @param labels: the json data of the label
* Ensures that the repo has the required labels.
* Compares the actual labels in the repo with the required labels. If an required label is missing,
* it is added to the repo. If the required label exists but the label color is not as expected,
* the color is updated. Does not delete actual labels that do not match required labels.
* i.e., the repo might have more labels than the required labels after this operation.
* @param actualLabels: labels in the repo.
* @param requiredLabels: required labels.
*/
private populateLabelLists(labels: Array<{}>): void {

for (const label of labels) {
// Get the name and color of each label and store them into the service's array list
const labelName = String(label['name']).split('.');
const labelType = labelName[0];
const labelValue = labelName[1];
const labelColor = String(label['color']);

// Check for duplicate labels
if (this.labelColorMap.has(labelValue)) {
continue;
private ensureRepoHasRequiredLabels(actualLabels: Label[], requiredLabels: Label[]): void {

requiredLabels.forEach(label => {

// Finds for a label that has the same name as a required label.
const nameMatchedLabels: Label[] = actualLabels.filter(remoteLabel =>
remoteLabel.getFormattedName() === label.getFormattedName());

if (nameMatchedLabels.length === 0) {
// Create new Label (Could not find a label with the same name & category)
this.githubService.createLabel(label.getFormattedName(), label.labelColor);
} else if (nameMatchedLabels.length === 1) {
if (nameMatchedLabels[0].equals(label)) {
// the label exists exactly as expected -> do nothing
} else {
// the label exists but the color does not match
this.githubService.updateLabel(label.getFormattedName(), label.labelColor);
}
} else {
throw new Error('Unexpected error: the repo has multiple labels with the same name ' + label.getFormattedName());
}

this.labelColorMap.set(labelValue, labelColor);
const labelList = this.getLabelList(labelType);
});
}

if (labelList !== undefined) {
labelList.push({labelValue: labelValue, labelColor: labelColor});
}
/**
* Parses label information and returns an array of Label objects.
* @param labels - Label Information from API.
*/
parseLabelData(labels: Array<{}>): Label[] {
const labelData: Label[] = [];
for (const label of labels) {

}
// Sort the severity labels from Low to High
this.severityLabels.sort((a, b) => {
return SEVERITY_ORDER[a.labelValue] - SEVERITY_ORDER[b.labelValue];
});
const labelName: string[] = String(label['name']).split('.');
const labelCategory: string = labelName[0];
const labelValue: string = labelName[1];
const labelColor: string = String(label['color']);

labelData.push(new Label(labelCategory, labelValue, labelColor));
}
return labelData;
}

reset(): void {
this.severityLabels.length = 0;
this.typeLabels.length = 0;
this.responseLabels.length = 0;
this.labelColorMap.clear();
}

/**
Expand Down

0 comments on commit 54c91e2

Please sign in to comment.