diff --git a/CHANGELOG.md b/CHANGELOG.md index 6580e81..6229bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,9 +69,8 @@ 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 -[2]: https://github.com/IJustDev/Gitea-VSCode/issues/2 \ No newline at end of file +[2]: https://github.com/IJustDev/Gitea-VSCode/issues/2 diff --git a/package.json b/package.json index cb02490..6904590 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Config.ts b/src/Config.ts deleted file mode 100644 index db54991..0000000 --- a/src/Config.ts +++ /dev/null @@ -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(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') - } -} diff --git a/src/IGiteaResponse.ts b/src/IGiteaResponse.ts new file mode 100644 index 0000000..60a47ed --- /dev/null +++ b/src/IGiteaResponse.ts @@ -0,0 +1,3 @@ +export interface IGiteaResponse { + data: any[]; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..03ea521 --- /dev/null +++ b/src/config.ts @@ -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(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') + } +} diff --git a/src/extension.ts b/src/extension.ts index ff6c16c..55c8333 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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 = []; - 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 = []; + 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() {} diff --git a/src/giteaConnector.ts b/src/giteaConnector.ts new file mode 100644 index 0000000..3174377 --- /dev/null +++ b/src/giteaConnector.ts @@ -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 { + return this.getEndpoint(`${repoUri}?state=${state}&page=${page}`); + } + + private async getEndpoint(url: string): Promise { + return new Promise((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 { + return new Promise((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); + } +} diff --git a/src/issue.ts b/src/issue.ts index ac71ed5..887624a 100644 --- a/src/issue.ts +++ b/src/issue.ts @@ -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); } diff --git a/src/issueProvider.ts b/src/issueProvider.ts index 666d895..86415bd 100644 --- a/src/issueProvider.ts +++ b/src/issueProvider.ts @@ -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 { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; +export class IssueProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - issueList: Issue[] = []; + readonly onDidChangeTreeData: vscode.Event = 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 { - return element; - } + public getTreeItem(element: Issue): vscode.TreeItem | Thenable { + 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 { - return getChildren(element, this.issueList); - } + + public async refresh() { + await this.getIssuesAsync(); + this._onDidChangeTreeData.fire(); + } + + public getChildren(element?: Issue): vscode.ProviderResult { + 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 { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = 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 { - 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 { - 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); - } -} diff --git a/src/template.html.ts b/src/template.html.ts index b4c743a..d713050 100644 --- a/src/template.html.ts +++ b/src/template.html.ts @@ -35,7 +35,7 @@ export function showIssueHTML(issue: Issue) { ` .replace('{{label}}', issue.label) - .replace('{{state}}', issue.issueState) + .replace('{{state}}', issue.state) .replace('{{assignee}}', issue.assignee) .replace('{{description}}', issue.body) .replace('{{label}}', issue.label);