Show github pull requests in the GitHub tab (#9148)
* WIP: prototype wiring up the API with the angular frontend * little refactorings in namespace and loading logic * frontend styling and polishing * pairing with aleix * Avoid empty pullRequest.githubUser to be displayed * added feature specs, a workaround for a pr-reload-bug in angular, and some readme improvements * Title for partial PRs added * GitActionsMenuComponent tests * Test improvements * GitHubTabComponent tests * TabHeaderComponent tests * TabPrsComponent tests * revert change to karma runner * start styling * i18n in component * avoid container element * git actions menu tests * Github interfaces * PullRequestComponent tests * Comment removed * Typings improvements * Merge fixes * Tests fixes * fix: wp list id links working with tabs * remove partial github pr state A PR might still be incomplete but enough data can be pulled from a github issuee notification to fill in most of the information * avoid duplicate spec * Fix import paths of op-icon Co-authored-by: Aleix Suau <info@macrofonoestudio.es> Co-authored-by: ulferts <jens.ulferts@googlemail.com> Co-authored-by: Oliver Günther <mail@oliverguenther.de>pull/9240/head
parent
34d2753743
commit
2f9d252456
@ -0,0 +1,88 @@ |
||||
import { |
||||
ComponentFixture, |
||||
TestBed, |
||||
} from '@angular/core/testing'; |
||||
import { DebugElement } from '@angular/core'; |
||||
import { GitActionsMenuComponent } from "./git-actions-menu.component"; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
import { OpContextMenuLocalsToken } from "core-app/components/op-context-menu/op-context-menu.types"; |
||||
import { GitActionsService } from "../git-actions/git-actions.service"; |
||||
import { By } from "@angular/platform-browser"; |
||||
import { OpIconComponent } from "core-app/modules/icon/icon.component"; |
||||
|
||||
|
||||
describe('GitActionsMenuComponent', () => { |
||||
let component:GitActionsMenuComponent; |
||||
let fixture:ComponentFixture<GitActionsMenuComponent>; |
||||
let element:DebugElement; |
||||
let gitActionsService:jasmine.SpyObj<GitActionsService>; |
||||
const I18nServiceStub = { |
||||
t: function(key:string) { |
||||
return 'test translation'; |
||||
} |
||||
} |
||||
const localsStub = { |
||||
workPackage: 1, |
||||
items: [ |
||||
{ |
||||
hidden: false, |
||||
disabled: false, |
||||
href: 'http://www.google.com', |
||||
linkText: 'linkText', |
||||
} |
||||
] |
||||
} |
||||
|
||||
beforeEach(async () => { |
||||
const gitActionsServiceSpy = jasmine.createSpyObj('GitActionsService', ['gitCommand', 'commitMessage', 'branchName']); |
||||
|
||||
await TestBed |
||||
.configureTestingModule({ |
||||
declarations: [ |
||||
GitActionsMenuComponent, |
||||
OpIconComponent, |
||||
], |
||||
providers: [ |
||||
{ provide: I18nService, useValue: I18nServiceStub }, |
||||
{ provide: OpContextMenuLocalsToken, useValue: localsStub }, |
||||
{ provide: GitActionsService, useValue: gitActionsServiceSpy }, |
||||
], |
||||
}) |
||||
.compileComponents(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
fixture = TestBed.createComponent(GitActionsMenuComponent); |
||||
component = fixture.componentInstance; |
||||
element = fixture.debugElement; |
||||
gitActionsService = fixture.debugElement.injector.get(GitActionsService) as jasmine.SpyObj<GitActionsService>; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
it('should create', () => { |
||||
expect(component).toBeTruthy(); |
||||
}); |
||||
|
||||
it('should select tab', () => { |
||||
const tabToSelect = component.tabs[0]; |
||||
component.selectTab(tabToSelect); |
||||
|
||||
fixture.detectChanges(); |
||||
|
||||
expect(component.selectedTab()).toBe(tabToSelect); |
||||
}); |
||||
|
||||
it('should select tab', () => { |
||||
const tabToSelect = component.tabs[0]; |
||||
const copyButton = fixture.debugElement.query(By.css('button')).nativeElement; |
||||
|
||||
gitActionsService.branchName.and.returnValue('test branch'); |
||||
component.selectTab(tabToSelect); |
||||
copyButton.click(); |
||||
|
||||
fixture.detectChanges(); |
||||
|
||||
expect(gitActionsService.branchName).toHaveBeenCalled(); |
||||
}); |
||||
}); |
@ -0,0 +1,60 @@ |
||||
import { |
||||
ComponentFixture, |
||||
TestBed, |
||||
} from '@angular/core/testing'; |
||||
import { DebugElement } from '@angular/core'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
import { GitHubTabComponent } from "core-app/modules/plugins/linked/openproject-github_integration/github-tab/github-tab.component"; |
||||
import { PathHelperService } from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import { TabPrsComponent } from "core-app/modules/plugins/linked/openproject-github_integration/tab-prs/tab-prs.component"; |
||||
import { TabHeaderComponent } from "core-app/modules/plugins/linked/openproject-github_integration/tab-header/tab-header.component"; |
||||
import { By } from "@angular/platform-browser"; |
||||
|
||||
|
||||
describe('GitHubTabComponent.', () => { |
||||
let component:GitHubTabComponent; |
||||
let fixture:ComponentFixture<GitHubTabComponent>; |
||||
let element:DebugElement; |
||||
const apiV3Base = 'http://www.openproject.com/api/v3/'; |
||||
const IPathHelperServiceStub = { api:{ v3: { apiV3Base }}}; |
||||
const I18nServiceStub = { |
||||
t: function(key:string) { |
||||
return 'test translation'; |
||||
} |
||||
} |
||||
|
||||
beforeEach(async () => { |
||||
await TestBed |
||||
.configureTestingModule({ |
||||
declarations: [ |
||||
TabPrsComponent, |
||||
TabHeaderComponent, |
||||
], |
||||
providers: [ |
||||
{ provide: I18nService, useValue: I18nServiceStub }, |
||||
{ provide: PathHelperService, useValue: IPathHelperServiceStub }, |
||||
], |
||||
}) |
||||
.compileComponents(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
fixture = TestBed.createComponent(GitHubTabComponent); |
||||
component = fixture.componentInstance; |
||||
element = fixture.debugElement; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
it('should create', () => { |
||||
expect(component).toBeTruthy(); |
||||
}); |
||||
|
||||
it('should render header and pull requests components', () => { |
||||
const tabHeader = fixture.debugElement.query(By.css('tab-header')); |
||||
const tabPrs = fixture.debugElement.query(By.css('tab-prs')); |
||||
|
||||
expect(tabHeader).toBeTruthy(); |
||||
expect(tabPrs).toBeTruthy(); |
||||
}); |
||||
}); |
@ -1,4 +1,2 @@ |
||||
<tab-header [workPackage]="workPackage"></tab-header> |
||||
<tab-prs [workPackage]="workPackage" |
||||
[pullRequests]="pullRequests" |
||||
></tab-prs> |
||||
<tab-prs [workPackage]="workPackage"></tab-prs> |
||||
|
@ -0,0 +1,42 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class GithubCheckRunResource extends HalResource { |
||||
public get state() { |
||||
return this.states.projects.get(this.id!) as any; |
||||
} |
||||
|
||||
/** |
||||
* Exclude the schema _link from the linkable Resources. |
||||
*/ |
||||
public $linkableKeys():string[] { |
||||
return _.without(super.$linkableKeys(), 'schema'); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class GithubPullRequestResource extends HalResource { |
||||
public get state() { |
||||
return this.states.projects.get(this.id!) as any; |
||||
} |
||||
|
||||
/** |
||||
* Exclude the schema _link from the linkable Resources. |
||||
*/ |
||||
public $linkableKeys():string[] { |
||||
return _.without(super.$linkableKeys(), 'schema'); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class GithubUserResource extends HalResource { |
||||
public get state() { |
||||
return this.states.projects.get(this.id!) as any; |
||||
} |
||||
|
||||
/** |
||||
* Exclude the schema _link from the linkable Resources. |
||||
*/ |
||||
public $linkableKeys():string[] { |
||||
return _.without(super.$linkableKeys(), 'schema'); |
||||
} |
||||
} |
@ -0,0 +1,109 @@ |
||||
//-- copyright |
||||
// OpenProject is an open source project management software. |
||||
// Copyright (C) 2012-2021 the OpenProject GmbH |
||||
// |
||||
// This program is free software; you can redistribute it and/or |
||||
// modify it under the terms of the GNU General Public License version 3. |
||||
// |
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
// Copyright (C) 2010-2013 the ChiliProject Team |
||||
// |
||||
// This program is free software; you can redistribute it and/or |
||||
// modify it under the terms of the GNU General Public License |
||||
// as published by the Free Software Foundation; either version 2 |
||||
// of the License, or (at your option) any later version. |
||||
// |
||||
// This program is distributed in the hope that it will be useful, |
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
// GNU General Public License for more details. |
||||
// |
||||
// You should have received a copy of the GNU General Public License |
||||
// along with this program; if not, write to the Free Software |
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
// |
||||
// See docs/COPYRIGHT.rdoc for more details. |
||||
//++ |
||||
|
||||
@import "helpers" |
||||
|
||||
.op-pr-pull-request |
||||
display: grid |
||||
grid-template-columns: auto 1fr auto auto |
||||
grid-template-areas: "title title title state" "avatar user link link" "avatar date link link" "avatar checks checks checks" |
||||
margin-bottom: 24px |
||||
|
||||
.op-pr-title |
||||
@include text-shortener |
||||
font-weight: bold |
||||
grid-area: title |
||||
// have the same line height as the status "button" next to it |
||||
line-height: 34px |
||||
margin-right: 20px |
||||
|
||||
.op-avatar |
||||
grid-area: avatar |
||||
|
||||
.op-pr-user |
||||
display: block |
||||
font-weight: bold |
||||
line-height: 16px |
||||
margin-bottom: 3px |
||||
grid-area: user |
||||
|
||||
.op-pr-date |
||||
display: block |
||||
font-size: 0.8rem |
||||
color: var(--gray-dark) |
||||
grid-area: date |
||||
|
||||
.op-pr-link |
||||
grid-area: link |
||||
margin-top: 6px |
||||
|
||||
.op-pr-state |
||||
grid-area: state |
||||
display: inline-block |
||||
padding: 8px |
||||
border-radius: 6px |
||||
border: 1px solid #fff |
||||
color: #fff |
||||
.op-pr-state_draft |
||||
background-color: #6a737d |
||||
.op-pr-state_open |
||||
background-color: #28a745 |
||||
.op-pr-state_merged |
||||
background-color: #6f42c1 |
||||
.op-pr-state_closed |
||||
background-color: #d73a49 |
||||
|
||||
.op-pr-checks |
||||
margin-top: 12px |
||||
grid-area: checks |
||||
margin-left: 0 |
||||
|
||||
&:before |
||||
content: attr(aria-label) |
||||
|
||||
.op-pr-check |
||||
list-style-type: none |
||||
padding-top: 8px |
||||
margin-left: 5px |
||||
|
||||
.op-pr-check-avatar img |
||||
width: 1.4em |
||||
height: 1.4em |
||||
margin-right: 5px |
||||
border-radius: var(--user-avatar-border-radius) |
||||
|
||||
.op-pr-check-state |
||||
margin-left: 1em |
||||
color: var(--gray-dark) |
||||
font-style: italic |
||||
|
||||
.op-pr-check-details |
||||
float: right |
||||
top: 10px |
||||
line-height: 16px |
||||
text-align: right |
@ -0,0 +1,109 @@ |
||||
import { |
||||
ComponentFixture, |
||||
TestBed, |
||||
} from '@angular/core/testing'; |
||||
import { Component, DebugElement, Input } from '@angular/core'; |
||||
import { By } from "@angular/platform-browser"; |
||||
import { PullRequestComponent } from "./pull-request.component"; |
||||
|
||||
@Component({ |
||||
selector: 'op-date-time', |
||||
template: ``, |
||||
}) |
||||
class OpDateTimeComponent { |
||||
@Input('dateTimeValue') dateTimeValue:any; |
||||
} |
||||
|
||||
describe('PullRequestComponent', () => { |
||||
let component:PullRequestComponent; |
||||
let fixture:ComponentFixture<PullRequestComponent>; |
||||
let element:DebugElement; |
||||
const githubUser = { |
||||
avatarUrl: 'testavatarurl', |
||||
htmlUrl: 'test htmlUrl', |
||||
login: 'test login', |
||||
}; |
||||
const checkRun = { |
||||
appOwnerAvatarUrl: 'test appOwnerAvatarUrl', |
||||
completedAt: 'test completedAt', |
||||
conclusion: 'test conclusion', |
||||
detailsUrl: 'testdetailsurl', |
||||
htmlUrl: 'test htmlUrl', |
||||
name: 'test name', |
||||
outputSummary: 'test outputSummary', |
||||
outputTitle: 'test outputTitle', |
||||
startedAt: 'test startedAt', |
||||
status: 'test status', |
||||
}; |
||||
const pullRequestStub = { |
||||
additionsCount: 3, |
||||
body:{ |
||||
format: '', |
||||
raw: 'test raw', |
||||
html:'<p>test</p>', |
||||
}, |
||||
changedFilesCount: 3, |
||||
commentsCount: 3, |
||||
createdAt: 'test createdAt', |
||||
deletionsCount: 3, |
||||
draft: false, |
||||
githubUpdatedAt: 'test githubUpdatedAt', |
||||
htmlUrl: 'test htmlUrl', |
||||
id: 3, |
||||
labels: ['test'], |
||||
merged: false, |
||||
mergedAt: false, |
||||
mergedBy: githubUser, |
||||
number: 3, |
||||
repository: 'test repository', |
||||
reviewCommentsCount: 3, |
||||
state: 'open', |
||||
title: 'test title', |
||||
updatedAt: 'test updatedAt', |
||||
githubUser, |
||||
checkRuns:[checkRun], |
||||
} |
||||
|
||||
beforeEach(async () => { |
||||
await TestBed |
||||
.configureTestingModule({ |
||||
declarations: [ |
||||
PullRequestComponent, |
||||
OpDateTimeComponent, |
||||
], |
||||
}) |
||||
.compileComponents(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
fixture = TestBed.createComponent(PullRequestComponent); |
||||
component = fixture.componentInstance; |
||||
element = fixture.debugElement; |
||||
// @ts-ignore
|
||||
component.pullRequest = pullRequestStub; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
it('should create', () => { |
||||
expect(component).toBeTruthy(); |
||||
}); |
||||
|
||||
it('should render pull request data', () => { |
||||
const titleElement = fixture.debugElement.query(By.css('.op-pr-title')).nativeElement; |
||||
const avatarElement = fixture.debugElement.query(By.css('.op-avatar')).nativeElement; |
||||
const userElement = fixture.debugElement.query(By.css('.op-pr-user')).nativeElement; |
||||
const detailsElement = fixture.debugElement.query(By.css('.op-pr-link')).nativeElement; |
||||
const checkRuns = fixture.debugElement.queryAll(By.css('.op-pr-check')); |
||||
const checkRunElement = checkRuns[0].nativeElement; |
||||
const checkRunLinkElement = checkRuns[0].query(By.css('a')).nativeElement; |
||||
|
||||
expect(titleElement.textContent).toContain(pullRequestStub.title); |
||||
expect(avatarElement.src).toContain(pullRequestStub.githubUser.avatarUrl); |
||||
expect(userElement.textContent).toContain(pullRequestStub.githubUser.login); |
||||
expect(detailsElement.textContent).toContain(`${pullRequestStub.repository}#${pullRequestStub.number}`); |
||||
expect(checkRuns.length).toBe(1); |
||||
expect(checkRunElement.textContent).toContain(pullRequestStub.checkRuns[0].name); |
||||
expect(checkRunLinkElement.href).toContain(pullRequestStub.checkRuns[0].detailsUrl); |
||||
}); |
||||
}); |
@ -0,0 +1,67 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { Component, Input } from '@angular/core'; |
||||
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
import { GithubCheckRunResource } from '../hal/resources/github-check-run-resource'; |
||||
import { IGithubPullRequestResource } from "../../../../../../../../modules/github_integration/frontend/module/typings"; |
||||
|
||||
@Component({ |
||||
selector: 'github-pull-request', |
||||
templateUrl: './pull-request.template.html', |
||||
styleUrls: ['./pull-request.component.sass'] |
||||
}) |
||||
export class PullRequestComponent { |
||||
@Input() public pullRequest:IGithubPullRequestResource; |
||||
|
||||
public text = { |
||||
label_updated_on: this.I18n.t('js.label_updated_on'), |
||||
label_details: this.I18n.t('js.label_details'), |
||||
label_actions: this.I18n.t('js.github_integration.github_actions'), |
||||
}; |
||||
|
||||
constructor(readonly PathHelper:PathHelperService, |
||||
readonly I18n:I18nService) { |
||||
} |
||||
|
||||
get state() { |
||||
if (this.pullRequest.state === 'open') { |
||||
return (this.pullRequest.draft ? 'draft' : 'open'); |
||||
} else { |
||||
return(this.pullRequest.merged ? 'merged' : 'closed'); |
||||
} |
||||
} |
||||
|
||||
public checkRunState(checkRun:GithubCheckRunResource) { |
||||
/* Github apps can *optionally* add an output object (and a title) which is the most relevant information to display. |
||||
If that is not present, we can display the conclusion (which is present only on finished runs). |
||||
If that is not present, we can always fall back to the status. */ |
||||
return(checkRun.outputTitle || checkRun.conclusion || checkRun.status); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
<div class='op-pr-pull-request'> |
||||
<div class='op-pr-title' |
||||
[textContent]="pullRequest.title"> |
||||
</div> |
||||
|
||||
<img alt='PR author avatar' |
||||
class='op-avatar' |
||||
[src]="pullRequest.githubUser.avatarUrl" |
||||
*ngIf="pullRequest.githubUser" |
||||
/> |
||||
<span class='op-pr-user'> |
||||
<a [href]="pullRequest.githubUser.htmlUrl" |
||||
[textContent]="pullRequest.githubUser.login" |
||||
*ngIf="pullRequest.githubUser" |
||||
> |
||||
</a> |
||||
</span> |
||||
|
||||
<span class='op-pr-date'> |
||||
{{ text.label_updated_on }} |
||||
<op-date-time [dateTimeValue]="pullRequest.githubUpdatedAt"></op-date-time> |
||||
</span> |
||||
|
||||
<span class='op-pr-state' [ngClass]="'op-pr-state_' + state">{{state}}</span> |
||||
<a class='op-pr-link' [href]="pullRequest.htmlUrl" [textContent]="pullRequest.repository + '#' + pullRequest.number"></a> |
||||
|
||||
<ul [attr.aria-label]="text.label_actions" class='op-pr-checks' *ngIf="pullRequest.checkRuns?.length"> |
||||
<li class='op-pr-check' *ngFor="let checkRun of pullRequest.checkRuns"> |
||||
<span class='op-pr-check-avatar'><img alt='app owner avatar' [src]="checkRun.appOwnerAvatarUrl" /></span> |
||||
|
||||
<span class='op-pr-check-name' [textContent]="checkRun.name"></span> |
||||
|
||||
<span class='op-pr-check-state' [textContent]="checkRunState(checkRun)"></span> |
||||
|
||||
<div class='op-pr-check-details'> |
||||
<a [href]="checkRun.detailsUrl"> |
||||
{{ text.label_details }} |
||||
</a> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,65 @@ |
||||
import { |
||||
ComponentFixture, |
||||
TestBed, |
||||
} from '@angular/core/testing'; |
||||
import { DebugElement } from '@angular/core'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
import { TabHeaderComponent } from "core-app/modules/plugins/linked/openproject-github_integration/tab-header/tab-header.component"; |
||||
import { By } from "@angular/platform-browser"; |
||||
import { OpIconComponent } from "core-app/modules/icon/icon.component"; |
||||
import { GitActionsMenuDirective } from "core-app/modules/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive"; |
||||
import { OPContextMenuService } from "core-components/op-context-menu/op-context-menu.service"; |
||||
|
||||
|
||||
describe('TabHeaderComponent', () => { |
||||
let component:TabHeaderComponent; |
||||
let fixture:ComponentFixture<TabHeaderComponent>; |
||||
let element:DebugElement; |
||||
const I18nServiceStub = { |
||||
t: function(key:string) { |
||||
return 'test translation'; |
||||
} |
||||
} |
||||
let oPContextMenuService:jasmine.SpyObj<OPContextMenuService>; |
||||
// @ts-ignore
|
||||
window.Mousetrap = () => () => {}; |
||||
|
||||
beforeEach(async () => { |
||||
const oPContextMenuServiceSpy = jasmine.createSpyObj('OPContextMenuService', ['show']); |
||||
|
||||
await TestBed |
||||
.configureTestingModule({ |
||||
declarations: [ |
||||
TabHeaderComponent, |
||||
OpIconComponent, |
||||
GitActionsMenuDirective, |
||||
], |
||||
providers: [ |
||||
{ provide: I18nService, useValue: I18nServiceStub }, |
||||
{ provide: OPContextMenuService, useValue: oPContextMenuServiceSpy }, |
||||
], |
||||
}) |
||||
.compileComponents(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
fixture = TestBed.createComponent(TabHeaderComponent); |
||||
component = fixture.componentInstance; |
||||
element = fixture.debugElement; |
||||
oPContextMenuService = fixture.debugElement.injector.get(OPContextMenuService) as jasmine.SpyObj<OPContextMenuService>; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
it('should create', () => { |
||||
expect(component).toBeTruthy(); |
||||
}); |
||||
|
||||
it('should render title and copy button', () => { |
||||
const headerTitle = fixture.debugElement.query(By.css('h3')).nativeElement; |
||||
const headerCopyButton = fixture.debugElement.query(By.css('button.github-git-copy[gitActionsCopyDropdown]')).nativeElement; |
||||
|
||||
expect(headerTitle.textContent.trim()).toBe('test translation'); |
||||
expect(headerCopyButton).toBeTruthy(); |
||||
}); |
||||
}); |
@ -0,0 +1,142 @@ |
||||
import { |
||||
ComponentFixture, |
||||
TestBed, |
||||
} from '@angular/core/testing'; |
||||
import { ChangeDetectorRef, DebugElement, Component, Input } from '@angular/core'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
import { OpIconComponent } from "core-app/modules/icon/icon.component"; |
||||
import { GitActionsMenuDirective } from "core-app/modules/plugins/linked/openproject-github_integration/git-actions-menu/git-actions-menu.directive"; |
||||
import { TabPrsComponent } from "core-app/modules/plugins/linked/openproject-github_integration/tab-prs/tab-prs.component"; |
||||
import { HalResourceService } from "core-app/modules/hal/services/hal-resource.service"; |
||||
import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; |
||||
import { of } from "rxjs"; |
||||
import { PullRequestComponent } from "core-app/modules/plugins/linked/openproject-github_integration/pull-request/pull-request.component"; |
||||
import { By } from "@angular/platform-browser"; |
||||
|
||||
@Component({ |
||||
selector: 'op-date-time', |
||||
template: '<p>OpDateTimeComponent </p>' |
||||
}) |
||||
class OpDateTimeComponent { |
||||
@Input() |
||||
dateTimeValue:any; |
||||
} |
||||
|
||||
describe('TabPrsComponent', () => { |
||||
let component:TabPrsComponent; |
||||
let fixture:ComponentFixture<TabPrsComponent>; |
||||
let element:DebugElement; |
||||
let halResourceService: jasmine.SpyObj<HalResourceService>; |
||||
let changeDetectorRef: jasmine.SpyObj<ChangeDetectorRef>; |
||||
const I18nServiceStub = { |
||||
t: function(key:string) { |
||||
return 'test translation'; |
||||
} |
||||
} |
||||
const APIV3Stub = { |
||||
work_packages: { |
||||
id: () => ({github_pull_requests: 'prpath'}) |
||||
} |
||||
} |
||||
const pullRequests = { |
||||
elements: [ |
||||
{ |
||||
title: 'title 1', |
||||
githubUser: { |
||||
avatarUrl: 'githubUser 1 avatarUrl', |
||||
htmlUrl: 'githubUser 1 htmlUrl', |
||||
login: 'githubUser 1 login', |
||||
}, |
||||
githubUpdatedAt: 'githubUser 1 githubUpdatedAt', |
||||
repository: 'githubUser 1 repository', |
||||
number: 'githubUser 1 number', |
||||
checkRuns: [ |
||||
{ |
||||
name: 'githubUser 1 checkRun', |
||||
outputTitle: 'githubUser 1 outputTitle', |
||||
conclusion: 'githubUser 1 conclusion', |
||||
status: 'githubUser 1 status', |
||||
detailsUrl: 'githubUser 1 detailsUrl', |
||||
} |
||||
], |
||||
}, |
||||
{ |
||||
title: 'title 2', |
||||
githubUser: { |
||||
avatarUrl: 'githubUser 2 avatarUrl', |
||||
htmlUrl: 'githubUser 2 htmlUrl', |
||||
login: 'githubUser 2 login', |
||||
}, |
||||
githubUpdatedAt: 'githubUser 2 githubUpdatedAt', |
||||
repository: 'githubUser 2 repository', |
||||
number: 'githubUser 2 number', |
||||
checkRuns: [ |
||||
{ |
||||
name: 'githubUser 2 checkRun', |
||||
outputTitle: 'githubUser 2 outputTitle', |
||||
conclusion: 'githubUser 2 conclusion', |
||||
status: 'githubUser 2 status', |
||||
detailsUrl: 'githubUser 2 detailsUrl', |
||||
} |
||||
], |
||||
} |
||||
] |
||||
} |
||||
|
||||
beforeEach(async () => { |
||||
const halResourceServiceSpy = jasmine.createSpyObj('HalResourceService', ['get']); |
||||
const changeDetectorSpy = jasmine.createSpyObj('ChangeDetectorRef', ['detectChanges']); |
||||
// @ts-ignore
|
||||
halResourceServiceSpy.get.and.returnValue(of(pullRequests)); |
||||
|
||||
await TestBed |
||||
.configureTestingModule({ |
||||
declarations: [ |
||||
TabPrsComponent, |
||||
OpIconComponent, |
||||
GitActionsMenuDirective, |
||||
PullRequestComponent, |
||||
OpDateTimeComponent, |
||||
], |
||||
providers: [ |
||||
{ provide: I18nService, useValue: I18nServiceStub }, |
||||
{ provide: HalResourceService, useValue: halResourceServiceSpy }, |
||||
{ provide: APIV3Service, useValue: APIV3Stub }, |
||||
{ provide: ChangeDetectorRef, useValue: changeDetectorSpy }, |
||||
], |
||||
}) |
||||
.compileComponents(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
fixture = TestBed.createComponent(TabPrsComponent); |
||||
component = fixture.componentInstance; |
||||
element = fixture.debugElement; |
||||
halResourceService = fixture.debugElement.injector.get(HalResourceService) as jasmine.SpyObj<HalResourceService>; |
||||
changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef) as jasmine.SpyObj<ChangeDetectorRef>; |
||||
// @ts-ignore
|
||||
component.workPackage = { id: 'testId' }; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
it('should create', () => { |
||||
expect(component).toBeTruthy(); |
||||
}); |
||||
|
||||
it('should display a PullRequestComponent per pull request', () => { |
||||
const pullRequests = fixture.debugElement.queryAll(By.css('github-pull-request')); |
||||
|
||||
expect(pullRequests.length).toBe(2); |
||||
}); |
||||
|
||||
it('should display a message when there are no pull requests', () => { |
||||
component.pullRequests = []; |
||||
fixture.detectChanges(); |
||||
const pullRequests = fixture.debugElement.queryAll(By.css('github-pull-request')); |
||||
const textMessage = fixture.debugElement.queryAll(By.css('p')); |
||||
|
||||
expect(pullRequests.length).toBe(0); |
||||
expect(textMessage).toBeTruthy(); |
||||
}); |
||||
}); |
@ -1,3 +1,7 @@ |
||||
<ng-container *ngIf="pullRequests.length === 0"> |
||||
<p [innerHTML]="getEmptyText()"></p> |
||||
</ng-container> |
||||
|
||||
<div *ngFor="let pullRequest of pullRequests"> |
||||
<github-pull-request [pullRequest]="pullRequest"></github-pull-request> |
||||
</div> |
||||
|
@ -0,0 +1,51 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { WorkPackageResource } from 'core-app/modules/hal/resources/work-package-resource'; |
||||
import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; |
||||
import { Injectable } from '@angular/core'; |
||||
import { ConfigurationService } from 'core-app/modules/common/config/configuration.service'; |
||||
import { WorkPackageLinkedResourceCache } from 'core-components/wp-single-view-tabs/wp-linked-resource-cache.service'; |
||||
|
||||
@Injectable() |
||||
export class WorkPackagesGithubPrsService extends WorkPackageLinkedResourceCache<HalResource[]> { |
||||
|
||||
constructor(public ConfigurationService:ConfigurationService) { |
||||
super(); |
||||
} |
||||
|
||||
protected load(workPackage:WorkPackageResource):Promise<HalResource[]> { |
||||
return workPackage.github_pull_requests.$update().then((data:any) => { |
||||
return this.sortList(data.elements); |
||||
}); |
||||
} |
||||
|
||||
protected sortList(pullRequests:HalResource[], attr = 'createdAt'):HalResource[] { |
||||
return _.sortBy(_.flatten(pullRequests), attr); |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
import { HalResource } from "../../../../frontend/src/app/modules/hal/resources/hal-resource"; |
||||
|
||||
export interface ITab { |
||||
label:string, |
||||
help:string, |
||||
selected:boolean, |
||||
lines:number, |
||||
textToCopy: ()=>string |
||||
} |
||||
|
||||
export interface IGithubPullRequestResource extends HalResource { |
||||
additionsCount?:number; |
||||
body?:{ |
||||
format?:string; |
||||
raw?:string; |
||||
html?:string; |
||||
}, |
||||
changedFilesCount?:number; |
||||
commentsCount?:number; |
||||
createdAt?:string; |
||||
deletionsCount?:number; |
||||
draft?:boolean; |
||||
githubUpdatedAt?:string; |
||||
htmlUrl?:string; |
||||
id?:number; |
||||
labels?:string[]; |
||||
merged?:boolean; |
||||
mergedAt?:string; |
||||
mergedBy?:IGithubUserResource; |
||||
number?:number; |
||||
repository?:string; |
||||
reviewCommentsCount?:number; |
||||
state?:string; |
||||
title?:string; |
||||
updatedAt?:string; |
||||
githubUser?:IGithubUserResource; |
||||
checkRuns?:IGithubCheckRunResource[]; |
||||
} |
||||
|
||||
export interface IGithubUserResource { |
||||
avatarUrl:string; |
||||
htmlUrl:string; |
||||
login:string; |
||||
} |
||||
|
||||
export interface IGithubCheckRunResource { |
||||
appOwnerAvatarUrl:string; |
||||
completedAt:string; |
||||
conclusion:string; |
||||
detailsUrl:string; |
||||
htmlUrl:string; |
||||
name:string; |
||||
outputSummary:string; |
||||
outputTitle:string; |
||||
startedAt:string; |
||||
status:string; |
||||
} |
Loading…
Reference in new issue