Rework code base; #17
This commit is contained in:
parent
cb4a963dfc
commit
858b8b7cda
@ -69,8 +69,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
|
||||
- Emojis in treeview
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
||||
- Rework code base
|
||||
|
||||
|
||||
[1]: https://github.com/IJustDev/Gitea-VSCode/issues/1
|
||||
|
@ -3,7 +3,7 @@
|
||||
"displayName": "Gitea-VSCode",
|
||||
"description": "Gitea Issue Tracker for vs-code",
|
||||
"publisher": "IJustDev",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"engines": {
|
||||
"vscode": "^1.32.0"
|
||||
},
|
||||
@ -119,8 +119,7 @@
|
||||
"vscode": "^1.1.28"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.1",
|
||||
"marked": "^0.6.2"
|
||||
"axios": "^0.18.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "github",
|
||||
|
@ -1,80 +0,0 @@
|
||||
import { workspace, window } from 'vscode';
|
||||
|
||||
interface ConfigStorage {
|
||||
token: string;
|
||||
instanceURL: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
sslVerify: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigTypes extends ConfigStorage {
|
||||
readonly repoApiUrl: string;
|
||||
}
|
||||
|
||||
export class Config implements ConfigTypes {
|
||||
private get storage() {
|
||||
return workspace.getConfiguration('gitea', null);
|
||||
}
|
||||
|
||||
private loadConfigValue<T extends keyof ConfigStorage>(configKey: T, type: 'string' | 'boolean' | 'number', acceptDetault = false): ConfigStorage[T] {
|
||||
if (!acceptDetault && !this.storage.has(configKey)) {
|
||||
window.showErrorMessage("Gitea-VSCode didn't find a required configuration value: " + configKey);
|
||||
throw new Error(`Failed to load configuration: "${configKey}"`);
|
||||
}
|
||||
|
||||
const value = this.storage.has(configKey)
|
||||
? (this.storage.get(configKey) as ConfigStorage[T])
|
||||
: (this.storage.inspect(configKey) as { defaultValue: ConfigStorage[T]; key: string }).defaultValue;
|
||||
|
||||
if (typeof value === type && (type !== 'string' || (value as string).length > 0)) {
|
||||
return value as ConfigStorage[T];
|
||||
}
|
||||
|
||||
window.showErrorMessage('Gitea-VSCode failed to load a configuration value that is needed: ' + configKey);
|
||||
throw new Error(`Failed to load configuration: "gitea.${configKey}"`);
|
||||
}
|
||||
|
||||
public get token() {
|
||||
return this.loadConfigValue('token', 'string');
|
||||
}
|
||||
|
||||
public set token(value) {
|
||||
this.storage.update('token', value);
|
||||
}
|
||||
|
||||
public set instanceUrl(value: string) {
|
||||
this.storage.update('instanceURL', value);
|
||||
}
|
||||
|
||||
public get instanceURL(): any {
|
||||
return this.loadConfigValue('instanceURL', 'string');
|
||||
}
|
||||
|
||||
public get owner() {
|
||||
return this.loadConfigValue('owner', 'string');
|
||||
}
|
||||
|
||||
public set owner(value) {
|
||||
this.storage.update('owner', value);
|
||||
}
|
||||
|
||||
public get repo() {
|
||||
return this.loadConfigValue('repo', 'string');
|
||||
}
|
||||
|
||||
public set repo(value) {
|
||||
this.storage.update('repo', value);
|
||||
}
|
||||
|
||||
public get repoApiUrl() {
|
||||
return this.instanceURL + '/api/v1/repos/' + this.owner + '/' + this.repo + '/issues';
|
||||
}
|
||||
|
||||
public set sslVerify(value){
|
||||
this.storage.update('sslVerify', value);
|
||||
}
|
||||
public get sslVerify(){
|
||||
return this.loadConfigValue('sslVerify', 'boolean')
|
||||
}
|
||||
}
|
3
src/IGiteaResponse.ts
Normal file
3
src/IGiteaResponse.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface IGiteaResponse {
|
||||
data: any[];
|
||||
}
|
81
src/config.ts
Normal file
81
src/config.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { workspace, window } from 'vscode';
|
||||
|
||||
interface ConfigStorage {
|
||||
token: string;
|
||||
instanceURL: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
sslVerify: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigTypes extends ConfigStorage {
|
||||
readonly repoApiUrl: string;
|
||||
}
|
||||
|
||||
export class Config implements ConfigTypes {
|
||||
private get storage() {
|
||||
return workspace.getConfiguration('gitea', null);
|
||||
}
|
||||
|
||||
private loadConfigValue<T extends keyof ConfigStorage>(configKey: T, type: 'string' | 'boolean' | 'number', acceptDetault = false): ConfigStorage[T] {
|
||||
if (!acceptDetault && !this.storage.has(configKey)) {
|
||||
window.showErrorMessage("Gitea-VSCode didn't find a required configuration value: " + configKey);
|
||||
throw new Error(`Failed to load configuration: "${configKey}"`);
|
||||
}
|
||||
|
||||
const value = this.storage.has(configKey)
|
||||
? (this.storage.get(configKey) as ConfigStorage[T])
|
||||
: (this.storage.inspect(configKey) as { defaultValue: ConfigStorage[T]; key: string }).defaultValue;
|
||||
|
||||
if (typeof value === type && (type !== 'string' || (value as string).length > 0)) {
|
||||
return value as ConfigStorage[T];
|
||||
}
|
||||
|
||||
window.showErrorMessage('Gitea-VSCode failed to load a configuration value that is needed: ' + configKey);
|
||||
throw new Error(`Failed to load configuration: "gitea.${configKey}"`);
|
||||
}
|
||||
|
||||
public get token() {
|
||||
return this.loadConfigValue('token', 'string');
|
||||
}
|
||||
|
||||
public set token(value) {
|
||||
this.storage.update('token', value);
|
||||
}
|
||||
|
||||
public set instanceUrl(value: string) {
|
||||
this.storage.update('instanceURL', value);
|
||||
}
|
||||
|
||||
public get instanceURL(): any {
|
||||
return this.loadConfigValue('instanceURL', 'string');
|
||||
}
|
||||
|
||||
public get owner() {
|
||||
return this.loadConfigValue('owner', 'string');
|
||||
}
|
||||
|
||||
public set owner(value) {
|
||||
this.storage.update('owner', value);
|
||||
}
|
||||
|
||||
public get repo() {
|
||||
return this.loadConfigValue('repo', 'string');
|
||||
}
|
||||
|
||||
public set repo(value) {
|
||||
this.storage.update('repo', value);
|
||||
}
|
||||
|
||||
public get repoApiUrl() {
|
||||
return this.instanceURL.replace(/\/$/, "") + '/api/v1/repos/' + this.owner + '/' + this.repo + '/issues';
|
||||
}
|
||||
|
||||
public set sslVerify(value) {
|
||||
this.storage.update('sslVerify', value);
|
||||
}
|
||||
|
||||
public get sslVerify() {
|
||||
return this.loadConfigValue('sslVerify', 'boolean')
|
||||
}
|
||||
}
|
@ -2,46 +2,40 @@ import * as vscode from 'vscode';
|
||||
|
||||
import { showIssueHTML } from './template.html';
|
||||
import { Issue } from './issue';
|
||||
import { OpenIssuesProvider, ClosedIssuesProvider } from './issueProvider';
|
||||
import { IssueProvider } from './issueProvider';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
let openIssues: Array<Issue> = [];
|
||||
const openIssuesProvider = new OpenIssuesProvider();
|
||||
const closedIssuesProvider = new ClosedIssuesProvider();
|
||||
|
||||
vscode.window.registerTreeDataProvider('open-issues', openIssuesProvider);
|
||||
vscode.window.registerTreeDataProvider('closed-issues', closedIssuesProvider);
|
||||
|
||||
// TODO: Implement in next version.
|
||||
// vscode.commands.registerCommand('giteaIssues.createIssue', async () => {
|
||||
// const panel = vscode.window.createWebviewPanel('createIssue', 'Create an new Issue', vscode.ViewColumn.Active, {});
|
||||
// panel.webview.html = "";
|
||||
// });
|
||||
|
||||
vscode.commands.registerCommand('giteaIssues.openIssue', (issue: Issue) => {
|
||||
for (let i = 0; i !== openIssues.length; i++) {
|
||||
let openIssue = openIssues[i];
|
||||
if (openIssue.issueId === issue.issueId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
export function showIssueInWebPanel(issue: Issue) {
|
||||
const panel = vscode.window.createWebviewPanel('issue', issue.label, vscode.ViewColumn.Active, {});
|
||||
panel.webview.html = showIssueHTML(issue);
|
||||
openIssues.push(issue);
|
||||
panel.onDidDispose((event) => {
|
||||
for (let i = 0; i !== openIssues.length; i++) {
|
||||
let openIssue = openIssues[i];
|
||||
if (openIssue.issueId === issue.issueId) {
|
||||
openIssues.splice(openIssues.indexOf(issue), 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
vscode.commands.registerCommand('giteaIssues.refreshIssues', () => {
|
||||
openIssuesProvider.refresh();
|
||||
closedIssuesProvider.refresh();
|
||||
});
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// Array of issues; This is used to determine whether a issue is already open
|
||||
// in a tab or not.
|
||||
let openIssues: Array<Issue> = [];
|
||||
const openIssuesProvider = new IssueProvider("open");
|
||||
const closedIssuesProvider = new IssueProvider("closed");
|
||||
|
||||
vscode.window.registerTreeDataProvider('open-issues', openIssuesProvider);
|
||||
vscode.window.registerTreeDataProvider('closed-issues', closedIssuesProvider);
|
||||
|
||||
vscode.commands.registerCommand('giteaIssues.openIssue', (issue: Issue) => {
|
||||
const issueOpenable = openIssues.find((c) => c.issueId === issue.issueId) === undefined;
|
||||
|
||||
if (issueOpenable) {
|
||||
const panel = showIssueInWebPanel(issue);
|
||||
openIssues.push(issue);
|
||||
panel.onDidDispose((event) => {
|
||||
openIssues.splice(openIssues.indexOf(issue), 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('giteaIssues.refreshIssues', () => {
|
||||
openIssuesProvider.refresh();
|
||||
closedIssuesProvider.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
|
50
src/giteaConnector.ts
Normal file
50
src/giteaConnector.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import axios from 'axios';
|
||||
|
||||
import { IGiteaResponse } from './IGiteaResponse';
|
||||
|
||||
export class GiteaConnector {
|
||||
private authToken: string;
|
||||
private ssl: boolean;
|
||||
|
||||
public constructor(authToken: string, ssl: boolean = false) {
|
||||
this.authToken = authToken;
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
public async getIssues(repoUri: string, state: string, page: number = 0): Promise<IGiteaResponse> {
|
||||
return this.getEndpoint(`${repoUri}?state=${state}&page=${page}`);
|
||||
}
|
||||
|
||||
private async getEndpoint(url: string): Promise<IGiteaResponse> {
|
||||
return new Promise<IGiteaResponse>((resolve, reject) => {
|
||||
return axios.get(url, this.requestOptions).then((data) => {
|
||||
resolve(data);
|
||||
}).catch((err) => {
|
||||
this.displayErrorMessage(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async postEndpoint(url: string): Promise<IGiteaResponse> {
|
||||
return new Promise<IGiteaResponse>((resolve, reject) => {
|
||||
return axios.post(url, this.requestOptions);
|
||||
});
|
||||
}
|
||||
|
||||
private get requestOptions(): object {
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: this.ssl,
|
||||
});
|
||||
return {
|
||||
headers: {Authorization: 'token ' + this.authToken},
|
||||
httpsAgent: agent,
|
||||
};
|
||||
}
|
||||
|
||||
private displayErrorMessage(err: string) {
|
||||
vscode.window.showErrorMessage("Error occoured. " + err);
|
||||
}
|
||||
}
|
@ -12,12 +12,12 @@ export class Issue extends TreeItem {
|
||||
public readonly label: string,
|
||||
public issueId: number,
|
||||
public body: string,
|
||||
public issueState: string,
|
||||
public state: string,
|
||||
public assignee: string,
|
||||
public creator: string,
|
||||
public labels: Label[],
|
||||
public collapsibleState: TreeItemCollapsibleState,
|
||||
public readonly command?: Command
|
||||
public command?: Command
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
}
|
||||
|
@ -1,181 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
|
||||
const marked = require('marked');
|
||||
import { Issue } from './issue';
|
||||
import { Config } from './Config';
|
||||
import { Config } from './config';
|
||||
import { GiteaConnector } from './giteaConnector';
|
||||
|
||||
export class OpenIssuesProvider implements vscode.TreeDataProvider<Issue> {
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<Issue | undefined> = new vscode.EventEmitter<Issue | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<Issue | undefined> = this._onDidChangeTreeData.event;
|
||||
export class IssueProvider implements vscode.TreeDataProvider<Issue> {
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<Issue | undefined> = new vscode.EventEmitter<Issue | undefined>();
|
||||
|
||||
issueList: Issue[] = [];
|
||||
readonly onDidChangeTreeData: vscode.Event<Issue | undefined> = this._onDidChangeTreeData.event;
|
||||
|
||||
async refresh() {
|
||||
await this.getChildrenAsync();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
private state: string;
|
||||
private issueList: Issue[] = [];
|
||||
|
||||
constructor() {
|
||||
// Auto update the issuelist after 10 minutes
|
||||
setInterval(() => {
|
||||
this.refresh();
|
||||
}, 10 * 60 * 1000);
|
||||
}
|
||||
constructor(state: string) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
getTreeItem(element: Issue): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element;
|
||||
}
|
||||
public getTreeItem(element: Issue): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all open issues;
|
||||
*/
|
||||
async getChildrenAsync() {
|
||||
this.issueList = [];
|
||||
const config = new Config();
|
||||
const repoUri = config.repoApiUrl;
|
||||
const token = config.token;
|
||||
let stop = false;
|
||||
public async getIssuesAsync() {
|
||||
this.issueList = [];
|
||||
const config = new Config();
|
||||
const giteaConnector = new GiteaConnector(config.token, config.sslVerify);
|
||||
|
||||
const agent = new https.Agent({
|
||||
// if true, stop when can't verify ssl
|
||||
rejectUnauthorized: config.sslVerify,
|
||||
});
|
||||
|
||||
for (let i = 0; i !== 10; i++) {
|
||||
await axios
|
||||
.get(repoUri + '?page=' + i, { headers: { Authorization: 'token ' + token }, httpsAgent: agent })
|
||||
.then((res) => {
|
||||
if (res.data.length === 0) {
|
||||
stop = true;
|
||||
return;
|
||||
}
|
||||
parseToIssues(res, this.issueList);
|
||||
})
|
||||
.catch((err) => {
|
||||
stop = true;
|
||||
vscode.window.showErrorMessage("An error occoured. Please check your repository url: " + repoUri);
|
||||
return;
|
||||
const issues = [];
|
||||
let page = 0;
|
||||
while (page < 11) {
|
||||
const issuesOfPage = (await giteaConnector.getIssues(config.repoApiUrl, this.state, page)).data;
|
||||
issues.push(...issuesOfPage);
|
||||
issuesOfPage.forEach((c) => {
|
||||
c.label = c.title;
|
||||
c.issueId = c.number;
|
||||
c.assignee = c.assignee === null ? 'Nobody' : c.assignee;
|
||||
c.creator = c.user.login;
|
||||
});
|
||||
page++;
|
||||
if (issues.length < 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.issueList = issues as Issue[];
|
||||
this.issueList.forEach((issue: Issue) => {
|
||||
issue.command = {
|
||||
command: 'giteaIssues.openIssue',
|
||||
title: '',
|
||||
arguments: [issue],
|
||||
};
|
||||
issue.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
});
|
||||
if (stop) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
getChildren(element?: Issue): vscode.ProviderResult<any[]> {
|
||||
return getChildren(element, this.issueList);
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
await this.getIssuesAsync();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
public getChildren(element?: Issue): vscode.ProviderResult<any[]> {
|
||||
return this.createChildNodes(element, this.issueList);
|
||||
}
|
||||
|
||||
private createChildNodes(element: Issue | undefined, issues: Issue[]) {
|
||||
for (const issue of issues) {
|
||||
if (element === issue) {
|
||||
let childItems: vscode.TreeItem[] = [
|
||||
new vscode.TreeItem('Assignee - ' + element.assignee, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('State - ' + element.state, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('ID - ' + element.issueId, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('From - ' + element.creator, vscode.TreeItemCollapsibleState.None),
|
||||
];
|
||||
return Promise.resolve(childItems);
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClosedIssuesProvider implements vscode.TreeDataProvider<Issue> {
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<Issue | undefined> = new vscode.EventEmitter<Issue | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<Issue | undefined> = this._onDidChangeTreeData.event;
|
||||
|
||||
issueList: Issue[] = [];
|
||||
|
||||
async refresh() {
|
||||
await this.getChildrenAsync();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
setInterval(() => {
|
||||
this.refresh();
|
||||
}, 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
getTreeItem(element: Issue): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element;
|
||||
}
|
||||
|
||||
async getChildrenAsync() {
|
||||
this.issueList = [];
|
||||
const config = new Config();
|
||||
const repoUri = config.repoApiUrl;
|
||||
const token = config.token;
|
||||
let stop = false;
|
||||
|
||||
const agent = new https.Agent({
|
||||
// if true, stop when can't verify ssl
|
||||
rejectUnauthorized: config.sslVerify,
|
||||
});
|
||||
|
||||
for (let i = 0; i !== 10; i++) {
|
||||
await axios
|
||||
.get(repoUri + '?state=closed&page=' + i, { headers: { Authorization: 'token ' + token } , httpsAgent: agent} )
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
if (res.data.length === 0) {
|
||||
stop = true;
|
||||
return;
|
||||
}
|
||||
parseToIssues(res, this.issueList);
|
||||
})
|
||||
.catch(() => {
|
||||
stop = true;
|
||||
vscode.window.showErrorMessage("An error occoured. Please check your repository url: " + repoUri);
|
||||
return;
|
||||
});
|
||||
if (stop) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
getChildren(element?: Issue): vscode.ProviderResult<any[]> {
|
||||
return getChildren(element, this.issueList);
|
||||
}
|
||||
}
|
||||
|
||||
export function getChildren(element: Issue | undefined, issueList: Issue[]) {
|
||||
for (const issue of issueList) {
|
||||
if (element === issue) {
|
||||
let childItems: vscode.TreeItem[] = [
|
||||
new vscode.TreeItem('👷 Assignee - ' + element.assignee, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('🚥 State - ' + element.issueState, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('🆔 ID - ' + element.issueId, vscode.TreeItemCollapsibleState.None),
|
||||
new vscode.TreeItem('✏️ From - ' + element.creator, vscode.TreeItemCollapsibleState.None),
|
||||
];
|
||||
return Promise.resolve(childItems);
|
||||
}
|
||||
}
|
||||
return issueList;
|
||||
}
|
||||
|
||||
export function parseToIssues(res: any, issueList: Issue[], collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Collapsed) {
|
||||
for (const issue of res.data) {
|
||||
const id = issue['number'];
|
||||
let isAlreadyInList = false;
|
||||
issueList.forEach((issueOfList) => {
|
||||
if (id === issueOfList.issueId) {
|
||||
isAlreadyInList = true;
|
||||
}
|
||||
});
|
||||
if (isAlreadyInList) {
|
||||
continue;
|
||||
}
|
||||
const title = issue['title'];
|
||||
const body = marked(issue['body']);
|
||||
const state = issue['state'];
|
||||
const assignee = issue['assignee'] === null ? 'None' : issue['assignee']['username'];
|
||||
const labels = issue['labels'];
|
||||
const creator = issue['user']['username'];
|
||||
const tmpIssue = new Issue('#' + id + ' - ' + title, id, body, state, assignee, creator, labels, collapsibleState);
|
||||
const issueForList = new Issue(
|
||||
tmpIssue.label,
|
||||
tmpIssue.issueId,
|
||||
tmpIssue.body,
|
||||
tmpIssue.issueState,
|
||||
tmpIssue.assignee,
|
||||
tmpIssue.creator,
|
||||
tmpIssue.labels,
|
||||
tmpIssue.collapsibleState,
|
||||
{
|
||||
command: 'giteaIssues.openIssue',
|
||||
title: '',
|
||||
arguments: [tmpIssue],
|
||||
}
|
||||
);
|
||||
issueList.push(issueForList);
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export function showIssueHTML(issue: Issue) {
|
||||
</body>
|
||||
`
|
||||
.replace('{{label}}', issue.label)
|
||||
.replace('{{state}}', issue.issueState)
|
||||
.replace('{{state}}', issue.state)
|
||||
.replace('{{assignee}}', issue.assignee)
|
||||
.replace('{{description}}', issue.body)
|
||||
.replace('{{label}}', issue.label);
|
||||
|
Loading…
Reference in New Issue
Block a user