Replace github state with store

This commit is contained in:
Oliver Günther
2023-03-15 16:18:53 +01:00
parent e5371a306f
commit 077112df43
21 changed files with 446 additions and 339 deletions
@@ -87,6 +87,9 @@ export class ApiV3Service {
// /api/v3/notifications
public readonly notifications = this.apiV3CustomEndpoint(ApiV3NotificationsPaths);
// /api/v3/github_pull_requests
public readonly github_pull_requests = this.apiV3CollectionEndpoint('github_pull_requests');
// /api/v3/grids
public readonly grids = this.apiV3CustomEndpoint(ApiV3GridsPaths);
@@ -68,6 +68,12 @@ class GithubPullRequest < ApplicationRecord
end
end
def visible?(user = User.current)
WorkPackage
.visible(user)
.exists?(id: work_packages.select(:id))
end
##
# When a PR lives long enough and receives many pushes, the same check (say, a CI test run) can be run multiple times.
# This method only returns the latest of each type of check_run.
@@ -27,9 +27,12 @@
//++
import copy from 'copy-text-to-clipboard';
import { Component, Inject, Input } from '@angular/core';
import {
Component,
Inject,
Input,
} from '@angular/core';
import { GitActionsService } from '../git-actions/git-actions.service';
import { ISnippet } from "core-app/features/plugins/linked/openproject-github_integration/typings";
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
import { OPContextMenuComponent } from "core-app/shared/components/op-context-menu/op-context-menu.component";
import {
@@ -37,14 +40,15 @@ import {
OpContextMenuLocalsToken,
} from "core-app/shared/components/op-context-menu/op-context-menu.types";
import { I18nService } from "core-app/core/i18n/i18n.service";
import { ISnippet } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.model';
@Component({
selector: 'op-git-actions-menu',
templateUrl: './git-actions-menu.template.html',
styleUrls: [
'./styles/git-actions-menu.sass'
]
'./styles/git-actions-menu.sass',
],
})
export class GitActionsMenuComponent extends OPContextMenuComponent {
@Input() public workPackage:WorkPackageResource;
@@ -54,38 +58,42 @@ export class GitActionsMenuComponent extends OPContextMenuComponent {
copyButtonHelpText: this.I18n.t('js.github_integration.tab_header.git_actions.copy_button_help'),
copyResult: {
success: this.I18n.t('js.github_integration.tab_header.git_actions.copy_success'),
error: this.I18n.t('js.github_integration.tab_header.git_actions.copy_error')
}
error: this.I18n.t('js.github_integration.tab_header.git_actions.copy_error'),
},
};
public lastCopyResult:string = this.text.copyResult.success;
public showCopyResult:boolean = false;
public copiedSnippetId:string = '';
public snippets:ISnippet[] = [
{
id: 'branch',
name: this.I18n.t('js.github_integration.tab_header.git_actions.branch_name'),
textToCopy: () => this.gitActions.branchName(this.workPackage)
textToCopy: () => this.gitActions.branchName(this.workPackage),
},
{
id: 'message',
name: this.I18n.t('js.github_integration.tab_header.git_actions.commit_message'),
textToCopy: () => this.gitActions.commitMessage(this.workPackage)
textToCopy: () => this.gitActions.commitMessage(this.workPackage),
},
{
id: 'command',
name: this.I18n.t('js.github_integration.tab_header.git_actions.cmd'),
textToCopy: () => this.gitActions.gitCommand(this.workPackage)
textToCopy: () => this.gitActions.gitCommand(this.workPackage),
},
];
constructor(@Inject(OpContextMenuLocalsToken)
public locals:OpContextMenuLocalsMap,
readonly I18n:I18nService,
readonly gitActions:GitActionsService) {
constructor(
@Inject(OpContextMenuLocalsToken)
public locals:OpContextMenuLocalsMap,
readonly I18n:I18nService,
readonly gitActions:GitActionsService,
) {
super(locals);
this.workPackage = this.locals.workPackage;
this.workPackage = this.locals.workPackage as WorkPackageResource;
}
public onCopyButtonClick(snippet:ISnippet):void {
@@ -1,2 +1,2 @@
<tab-header [workPackage]="workPackage"></tab-header>
<tab-prs [workPackage]="workPackage"></tab-prs>
<op-tab-prs [workPackage]="workPackage"></op-tab-prs>
@@ -1,42 +0,0 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
//++
import { HalResource } from "core-app/features/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');
}
}
@@ -1,42 +0,0 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
//++
import { HalResource } from "core-app/features/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');
}
}
@@ -1,42 +0,0 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
//++
import { HalResource } from "core-app/features/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');
}
}
@@ -33,19 +33,19 @@ import { TabHeaderComponent } from './tab-header/tab-header.component';
import { TabPrsComponent } from './tab-prs/tab-prs.component';
import { GitActionsMenuDirective } from './git-actions-menu/git-actions-menu.directive';
import { GitActionsMenuComponent } from './git-actions-menu/git-actions-menu.component';
import { WorkPackagesGithubPrsService } from './tab-prs/wp-github-prs.service';
import { PullRequestComponent } from './pull-request/pull-request.component';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { GithubPullRequestResourceService } from './state/github-pull-request.service';
export function workPackageGithubPrsCount(
workPackage:WorkPackageResource,
injector:Injector,
):Observable<number> {
const githubPrsService = injector.get(WorkPackagesGithubPrsService);
const githubPrsService = injector.get(GithubPullRequestResourceService);
return githubPrsService
.requireAndStream(workPackage)
.ofWorkPackage(workPackage)
.pipe(
map((prs) => prs.length),
);
@@ -68,7 +68,7 @@ export function initializeGithubIntegrationPlugin(injector:Injector) {
OpenprojectTabsModule,
],
providers: [
WorkPackagesGithubPrsService,
GithubPullRequestResourceService,
],
declarations: [
GitHubTabComponent,
@@ -0,0 +1,115 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
// ++ Ng1FieldControlsWrapper,
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
HostBinding,
Injector,
Input,
} from '@angular/core';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
import { DisplayFieldService } from 'core-app/shared/components/fields/display/display-field.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import {
AttributeModelLoaderService,
SupportedAttributeModels,
} from 'core-app/shared/components/fields/macros/attribute-model-loader.service';
import { capitalize } from 'core-app/shared/helpers/string-helpers';
import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs';
export const githubPullRequestMacroSelector = 'macro.github-pull-request';
@Component({
selector: githubPullRequestMacroSelector,
templateUrl: './pull-request-macro.component.html',
styleUrls: ['./pull-request-macro.component.sass'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
HalResourceEditingService,
],
})
export class PullRequestMacroComponent {
@Input() pullRequestId:string;
constructor(
readonly elementRef:ElementRef,
readonly injector:Injector,
readonly resourceLoader:AttributeModelLoaderService,
readonly schemaCache:SchemaCacheService,
readonly displayField:DisplayFieldService,
readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef) {
populateInputsFromDataset(this);
}
ngOnInit() {
const element = this.elementRef.nativeElement as HTMLElement;
const model:SupportedAttributeModels = element.dataset.model as any;
const id:string = element.dataset.id!;
const attributeName:string = element.dataset.attribute!;
this.attributeScope = capitalize(model);
this.loadResourceAttribute(model, id, attributeName);
}
private async loadResourceAttribute(model:SupportedAttributeModels, id:string, attributeName:string) {
let resource:HalResource|null;
try {
this.resource = resource = await this.resourceLoader.require(model, id);
} catch (e) {
console.error(`Failed to render macro ${e}`);
return this.markError(this.text.not_found);
}
if (!resource) {
this.markError(this.text.not_found);
return;
}
const schema = await this.schemaCache.ensureLoaded(resource);
this.attribute = schema.attributeFromLocalizedName(attributeName) || attributeName;
this.label = schema[this.attribute]?.name;
if (!this.label) {
this.markError(this.text.invalid_attribute(attributeName));
}
this.cdRef.detectChanges();
}
markError(message:string) {
this.error = this.I18n.t('js.editor.macro.error', { message });
this.cdRef.detectChanges();
}
}
@@ -13,16 +13,16 @@
<div class="op-pull-request--info">
{{ text.label_created_by }}
<img
*ngIf="pullRequest._embedded.githubUser"
alt='PR author avatar'
class='op-pull-request--avatar op-avatar op-avatar_mini'
[src]="pullRequest.githubUser.avatarUrl"
*ngIf="pullRequest.githubUser"
[src]="pullRequest._embedded.githubUser.avatarUrl"
/>
<span class='op-pull-request--user'>
<a
[href]="pullRequest.githubUser.htmlUrl"
[textContent]="pullRequest.githubUser.login"
*ngIf="pullRequest.githubUser"
*ngIf="pullRequest._embedded.githubUser"
[href]="pullRequest._embedded.githubUser.htmlUrl"
[textContent]="pullRequest._embedded.githubUser.login"
></a>.
</span>
@@ -37,15 +37,22 @@
{{state}}
</span>
<span class="op-pull-request--checks-label" *ngIf="pullRequest.checkRuns?.length">{{ text.label_actions }}</span>
<span class="op-pull-request--checks-label"
*ngIf="pullRequest._embedded.checkRuns?.length"
[textContent]="text.label_actions"
></span>
<ul [attr.aria-label]="text.label_actions" class='op-pull-request--checks' *ngIf="pullRequest.checkRuns?.length">
<li class='op-pr-check' *ngFor="let checkRun of pullRequest.checkRuns">
<ul
*ngIf="pullRequest._embedded.checkRuns?.length"
[attr.aria-label]="text.label_actions"
class='op-pull-request--checks'
>
<li class='op-pr-check' *ngFor="let checkRun of pullRequest._embedded.checkRuns">
<span class='op-pr-check--state-icon' [ngClass]="'op-pr-check--state-icon_' + checkRunState(checkRun)">
<op-icon icon-classes="icon-{{ checkRunStateIcon(checkRun) }}"
[icon-title]="checkRunStateText(checkRun)"></op-icon>
</span>
<span class='op-pr-check--avatar'><img alt='app owner avatar' [src]="checkRun.appOwnerAvatarUrl" /></span>
<span class='op-pr-check--avatar'><img alt='app owner avatar' [src]="checkRun.appOwnerAvatarUrl"/></span>
<span class='op-pr-check--info'>
<span class='op-pr-check--name' [textContent]="checkRun.name"></span>
@@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 the OpenProject GmbH
//
@@ -26,24 +26,33 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { Component, Input } from '@angular/core';
import { GithubCheckRunResource } from 'core-app/features/plugins/linked/openproject-github_integration/hal/resources/github-check-run-resource';
import { IGithubPullRequestResource } from "core-app/features/plugins/linked/openproject-github_integration/typings";
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
import { I18nService } from "core-app/core/i18n/i18n.service";
import {
ChangeDetectionStrategy,
Component,
HostBinding,
Input,
} from '@angular/core';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import {
IGithubCheckRunResource,
IGithubPullRequest,
} from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.model';
@Component({
selector: 'github-pull-request',
selector: 'op-github-pull-request',
templateUrl: './pull-request.component.html',
styleUrls: [
'./pull-request.component.sass',
'./pr-check.component.sass',
],
host: { class: 'op-pull-request' }
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PullRequestComponent {
@Input() public pullRequest:IGithubPullRequestResource;
@HostBinding('class.op-pull-request') className = true;
@Input() public pullRequest:IGithubPullRequest;
public text = {
label_created_by: this.I18n.t('js.label_created_by'),
@@ -52,63 +61,64 @@ export class PullRequestComponent {
label_actions: this.I18n.t('js.github_integration.github_actions'),
};
constructor(readonly PathHelper:PathHelperService,
readonly I18n:I18nService) {
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');
}
return (this.pullRequest.merged ? 'merged' : 'closed');
}
public checkRunStateText(checkRun:GithubCheckRunResource) {
public checkRunStateText(checkRun:IGithubCheckRunResource) {
/* 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);
return (checkRun.outputTitle || checkRun.conclusion || checkRun.status);
}
public checkRunState(checkRun:GithubCheckRunResource) {
return(checkRun.conclusion || checkRun.status);
public checkRunState(checkRun:IGithubCheckRunResource) {
return (checkRun.conclusion || checkRun.status);
}
public checkRunStateIcon(checkRun:GithubCheckRunResource) {
public checkRunStateIcon(checkRun:IGithubCheckRunResource) {
switch (this.checkRunState(checkRun)) {
case 'success': {
return 'checkmark'
return 'checkmark';
}
case 'queued': {
return 'getting-started'
return 'getting-started';
}
case 'in_progress': {
return 'loading1'
return 'loading1';
}
case 'failure': {
return 'cancel'
return 'cancel';
}
case 'timed_out': {
return 'reminder'
return 'reminder';
}
case 'action_required': {
return 'warning'
return 'warning';
}
case 'stale': {
return 'not-supported'
return 'not-supported';
}
case 'skipped': {
return 'redo'
return 'redo';
}
case 'neutral': {
return 'minus1'
return 'minus1';
}
case 'cancelled': {
return 'minus1'
return 'minus1';
}
default: {
return 'not-supported'
return 'not-supported';
}
}
}
@@ -1,4 +1,8 @@
import { HalResourceClass } from 'core-app/modules/hal/resources/hal-resource';
import {
IHalResourceLink,
IHalResourceLinks,
} from 'core-app/core/state/hal-resource';
import { ID } from '@datorama/akita';
export interface ISnippet {
id:string;
@@ -6,35 +10,6 @@ export interface ISnippet {
textToCopy:() => string
}
export interface IGithubPullRequestResource extends HalResourceClass {
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;
@@ -53,3 +28,44 @@ export interface IGithubCheckRunResource {
startedAt:string;
status:string;
}
export interface IGithubPullRequestResourceLinks extends IHalResourceLinks {
githubUser:IHalResourceLink;
mergedBy?:IHalResourceLink;
checkRuns?:IHalResourceLink[];
}
export interface IGithubPullRequestResourceEmbedded {
githubUser?:IGithubUserResource;
mergedBy?:IGithubUserResource;
checkRuns?:IGithubCheckRunResource[];
}
export interface IGithubPullRequest {
id:ID;
additionsCount?:number;
body?:{
format?:string;
raw?:string;
html?:string;
},
changedFilesCount?:number;
commentsCount?:number;
createdAt?:string;
deletionsCount?:number;
draft?:boolean;
githubUpdatedAt?:string;
htmlUrl?:string;
labels?:string[];
merged?:boolean;
mergedAt?:string;
number?:number;
repository?:string;
reviewCommentsCount?:number;
state?:string;
title?:string;
updatedAt?:string;
_links:IGithubPullRequestResourceLinks;
_embedded:IGithubPullRequestResourceEmbedded;
}
@@ -0,0 +1,75 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
//++
import { Injectable } from '@angular/core';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { Observable } from 'rxjs';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import {
CollectionStore,
ResourceCollectionLoadOptions,
ResourceCollectionService,
} from 'core-app/core/state/resource-collection.service';
import { IGithubPullRequest } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.model';
import { GithubPullRequestsStore } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.store';
import { ID } from '@datorama/akita';
@Injectable()
export class GithubPullRequestResourceService extends ResourceCollectionService<IGithubPullRequest> {
ofWorkPackage(workPackage:WorkPackageResource) {
return this.requireEntity(`${workPackage.href as string}/github_pull_requests`);
}
requireSingle(id:ID) {
return this.requireEntity(this.entityPath(id));
}
fetchCollection(
params:ApiV3ListParameters|string,
options:ResourceCollectionLoadOptions = { handleErrors: true },
):Observable<IHALCollection<IGithubPullRequest>> {
if (typeof params !== 'string') {
throw new Error('Github PR service can only deal with string collection keys being their full paths')
}
return this.request(params, params, options);
}
protected basePath():string {
return this.apiV3Service.github_pull_requests.path;
}
protected entityPath(id:ID) {
return this.apiV3Service.github_pull_requests.id(id).path;
}
protected createStore():CollectionStore<IGithubPullRequest> {
return new GithubPullRequestsStore();
}
}
@@ -0,0 +1,13 @@
import { EntityStore, StoreConfig } from '@datorama/akita';
import { CollectionState, createInitialCollectionState } from 'core-app/core/state/collection-store';
import { IGithubPullRequest } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.model';
export interface GithubPullRequestsState extends CollectionState<IGithubPullRequest> {
}
@StoreConfig({ name: 'github-pull-requests' })
export class GithubPullRequestsStore extends EntityStore<GithubPullRequestsState> {
constructor() {
super(createInitialCollectionState());
}
}
@@ -0,0 +1,8 @@
<ng-container *ngIf="(pullRequests$ | async)?.length === 0">
<p [innerHTML]="getEmptyText()"></p>
</ng-container>
<op-github-pull-request
*ngFor="let pullRequest of (pullRequests$ | async)"
[pullRequest]="pullRequest"
></op-github-pull-request>
@@ -1,4 +1,4 @@
//-- copyright
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 the OpenProject GmbH
//
@@ -26,43 +26,52 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
import {
ChangeDetectionStrategy,
Component,
HostBinding,
Input,
OnInit,
} from '@angular/core';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service";
import { CollectionResource } from "core-app/features/hal/resources/collection-resource";
import { IGithubPullRequestResource } from "../../../../../../../../modules/github_integration/frontend/module/typings";
import { I18nService } from "core-app/core/i18n/i18n.service";
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { GithubPullRequestResourceService } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.service';
import { IGithubPullRequest } from 'core-app/features/plugins/linked/openproject-github_integration/state/github-pull-request.model';
import {
map,
shareReplay,
} from 'rxjs/operators';
import { Observable } from 'rxjs';
@Component({
selector: 'tab-prs',
templateUrl: './tab-prs.template.html',
host: { class: 'op-prs' }
selector: 'op-tab-prs',
templateUrl: './tab-prs.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabPrsComponent implements OnInit {
@Input() public workPackage:WorkPackageResource;
@HostBinding('class.op-github-prs') className = true;
public pullRequests:IGithubPullRequestResource[] = [];
@Input() workPackage:WorkPackageResource;
pullRequests$:Observable<IGithubPullRequest[]>;
emptyText:string;
constructor(
readonly I18n:I18nService,
readonly apiV3Service:ApiV3Service,
readonly halResourceService:HalResourceService,
readonly changeDetector:ChangeDetectorRef,
readonly githubPullRequests:GithubPullRequestResourceService,
) {}
ngOnInit(): void {
const pullRequestsPath = this.apiV3Service.work_packages.id({id: this.workPackage.id })?.github_pull_requests.path;
this.halResourceService
.get<CollectionResource<IGithubPullRequestResource>>(pullRequestsPath)
.subscribe((value) => {
this.pullRequests = value.elements;
this.changeDetector.detectChanges();
});
}
public getEmptyText() {
return this.I18n.t('js.github_integration.tab_prs.empty',{ wp_id: this.workPackage.id });
ngOnInit():void {
this.emptyText = this.I18n.t('js.github_integration.tab_prs.empty', { wp_id: this.workPackage.id });
this.pullRequests$ = this
.githubPullRequests
.ofWorkPackage(this.workPackage)
.pipe(
map((elements) => _.sortBy(elements, 'updatedAt')),
shareReplay(1),
);
}
}
@@ -1,5 +0,0 @@
<ng-container *ngIf="pullRequests.length === 0">
<p [innerHTML]="getEmptyText()"></p>
</ng-container>
<github-pull-request [pullRequest]="pullRequest" *ngFor="let pullRequest of pullRequests"></github-pull-request>
@@ -1,51 +0,0 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
//++
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
import { HalResource } from "core-app/features/hal/resources/hal-resource";
import { Injectable } from '@angular/core';
import { ConfigurationService } from "core-app/core/config/configuration.service";
import { WorkPackageLinkedResourceCache } from 'core-app/features/work-packages/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,51 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 COPYRIGHT and LICENSE files for more details.
#++
module API
module V3
module GithubPullRequests
class GithubPullRequestsAPI < ::API::OpenProjectAPI
resources :github_pull_requests do
route_param :id, type: Integer, desc: 'Pull Request ID' do
after_validation do
@pull_request = GithubPullRequest.find(declared_params[:id])
authorize_by_with_raise @pull_request.visible?(current_user) do
raise API::Errors::NotFound
end
end
get &::API::V3::Utilities::Endpoints::Show.new(model: ::GithubPullRequest,
instance_generator: ->(*) { @pull_request })
.mount
end
end
end
end
end
end
@@ -83,6 +83,10 @@ module OpenProject::GithubIntegration
mount ::API::V3::GithubPullRequests::GithubPullRequestsByWorkPackageAPI
end
add_api_endpoint 'API::V3::Root' do
mount ::API::V3::GithubPullRequests::GithubPullRequestsAPI
end
config.to_prepare do
# Register the cron job to clean up old github pull requests
::Cron::CronJob.register! ::Cron::ClearOldPullRequestsJob
@@ -45,19 +45,20 @@ module OpenProject::GithubIntegration
github_system_user = User.find_by(id: payload.open_project_user_id)
work_packages = find_mentioned_work_packages(payload.pull_request.body, github_system_user)
pull_request = upsert_pull_request(work_packages)
comment_on_referenced_work_packages(
work_packages_to_comment_on(payload.action, work_packages),
work_packages_to_comment_on(payload.action, pull_request, work_packages),
github_system_user,
journal_entry
journal_entry(pull_request)
)
upsert_pull_request(work_packages)
end
private
attr_reader :payload
def work_packages_to_comment_on(action, work_packages)
def work_packages_to_comment_on(action, pull_request, work_packages)
if action == 'edited'
without_already_referenced(work_packages, pull_request)
else
@@ -65,13 +66,6 @@ module OpenProject::GithubIntegration
end
end
def pull_request
@pull_request ||= GithubPullRequest
.where(github_id: payload.pull_request.id)
.or(GithubPullRequest.where(github_html_url: payload.pull_request.html_url))
.take
end
def upsert_pull_request(work_packages)
return if work_packages.empty? && pull_request.nil?
@@ -79,38 +73,8 @@ module OpenProject::GithubIntegration
work_packages:)
end
def journal_entry
key = journal_entry_i18n_key
return nil unless key
pull_request = payload.pull_request
repository = pull_request.base.repo
sender = payload.sender
I18n.t("github_integration.pull_request_#{key}_comment",
pr_number: pull_request.number,
pr_title: pull_request.title,
pr_url: pull_request.html_url,
repository: repository.full_name,
repository_url: repository.html_url,
github_user: sender.login,
github_user_url: sender.html_url)
end
def journal_entry_i18n_key
key = {
'opened' => 'opened',
'reopened' => 'opened',
'closed' => 'closed',
'edited' => 'referenced',
'referenced' => 'referenced',
'ready_for_review' => 'ready_for_review'
}[payload.action]
return 'merged' if key == 'closed' && payload.pull_request.merged
return 'draft' if key == 'open' && payload.pull_request.draft
key
def journal_entry(pull_request)
%(<macro class="github_pull_request" data-pull-request-id="#{pull_request.id}"></macro>)
end
end
end