From e45cba2a842f59bf0a63bfa36c2984487a688c1b Mon Sep 17 00:00:00 2001 From: goodhumored Date: Mon, 22 May 2023 19:29:39 +0300 Subject: [PATCH 01/15] views container added --- package.json | 32 ++++++++++++++++++++++++++++++-- resources/icon.png | Bin 0 -> 720 bytes src/extension.ts | 7 ++++--- webpack.config.js | 20 +++++++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 resources/icon.png diff --git a/package.json b/package.json index 4add615..ff0468d 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,36 @@ "contributes": { "commands": [ { - "command": "openproject.helloWorld", - "title": "Hello World" + "command": "openproject.auth", + "title": "Authorize", + "shortTitle": "Auth" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "openproject", + "icon": "resources/icon.png", + "title": "OpenProject" + } + ] + }, + "views": { + "openproject": [ + { + "id": "openproject-workspaces", + "name": "Workspaces" + } + ] + }, + "viewsWelcome": [ + { + "id": "openproject-login", + "name": "Auth", + "view": "openproject-workspaces", + "type": "webview", + "contents": "Hello! To use OpenProject features you need to authorize:\n[Auth](command:openproject.auth)", + "when": "!openproject.authed" } ] }, diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..22e855ee0c852a5f1ec41331d2462dd1161e0d81 GIT binary patch literal 720 zcmV;>0x$iEP)2x)$qYNsgWV8Ao0z2tey4fw zoB6((dGm&VOHfLO5{blvApii) zX7hVDFNCNeqBu2E+!bK}{Fer5r|yLtgu~&xUa!{+0I8+=+4;&j zueKA_YPE@oxV5$QMNyQY?gg5rX=A5gj4@SJRVUUoP3!CE9IsR=y*rCHP1AJd+&YC5 zo2F^@({DdhM3jAZ;{yM#lL;a8oH>8wg%DIqi2wlgdcD=x;pph-CnDnZ_I8<4da0W) z7KDCOG|}`iHSS9uIr&t=yocV zns>O(&CR{;)BF{};qbkalap4j1`#}lDYsC}=yySv|(mzN9Wa`}_n&Hn;)2qO3^RD%cr0000 { - vscode.window.showInformationMessage('Hello World from OpenProject!'); + let disposable = vscode.commands.registerCommand('openproject.auth', () => { + vscode.window.showInputBox({ignoreFocusOut: true, password: true, prompt: "API token", title: "OpenProject API token"}).then(token => { + if (token) {vscode.window.showInformationMessage(`Hello, ${token}!`);} + }); }); - context.subscriptions.push(disposable); } export function deactivate() {} diff --git a/webpack.config.js b/webpack.config.js index 736707a..b000c95 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,7 +20,6 @@ const extensionConfig = { vscode: 'commonjs vscode' }, resolve: { - extensions: ['.ts', '.js'] }, module: { @@ -33,6 +32,14 @@ const extensionConfig = { loader: 'ts-loader' } ] + }, + { + test: /resources\/*/, + use: [ + { + loader: 'file-loader' + } + ] } ] }, @@ -40,5 +47,16 @@ const extensionConfig = { infrastructureLogging: { level: "log", }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: ".", + to: "resources", + context: "resources" + } + ] + }) + ] }; module.exports = [ extensionConfig ]; \ No newline at end of file From 90252116efc0eb68dac7c47a5a7ccd2f6148e87f Mon Sep 17 00:00:00 2001 From: goodhumored Date: Mon, 22 May 2023 19:55:07 +0300 Subject: [PATCH 02/15] auth implemented --- package.json | 14 +++++++++++++- src/extension.ts | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ff0468d..1f0c936 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,19 @@ "view": "openproject-workspaces", "type": "webview", "contents": "Hello! To use OpenProject features you need to authorize:\n[Auth](command:openproject.auth)", - "when": "!openproject.authed" + "when": "!openproject.token" + } + ], + "configuration":[ + { + "title": "OpenProject", + "properties": { + "openproject.token": { + "type": "string", + "default": null, + "description": "OpenProject API token" + } + } } ] }, diff --git a/src/extension.ts b/src/extension.ts index a0acb91..6c537b0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,9 +5,12 @@ export function activate(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand('openproject.auth', () => { vscode.window.showInputBox({ignoreFocusOut: true, password: true, prompt: "API token", title: "OpenProject API token"}).then(token => { - if (token) {vscode.window.showInformationMessage(`Hello, ${token}!`);} + if (!token) {return;} + vscode.window.showInformationMessage(`Hello, ${token}!`); + vscode.commands.executeCommand("setContext", "openproject.token", token); }); }); + context.subscriptions.push(disposable); } export function deactivate() {} From a738d7a958ffcf4091e0556300157ac7c0f75b67 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Wed, 24 May 2023 02:33:20 +0300 Subject: [PATCH 03/15] eslint rules updated --- .eslintrc.json | 280 +++++++++++++++++++++++++++++++++++++++++++++---- .prettierrc | 10 ++ package.json | 64 +++++++++-- 3 files changed, 322 insertions(+), 32 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.json b/.eslintrc.json index f9b22b7..3a78a61 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,24 +1,260 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/naming-convention": "warn", - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" - }, - "ignorePatterns": [ - "out", - "dist", - "**/*.d.ts" + "env": { + "browser": false, + "es2021": true + }, + "extends": [ + "prettier", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "airbnb-typescript/base", + "airbnb-base", + "plugin:sonarjs/recommended", + "plugin:jsdoc/recommended-typescript" + ], + "overrides": [ + { + "files": [ + "**/*.spec.ts" + ], + "env": { + "jest": true + } + } + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": [ + "./tsconfig.json" ] -} + }, + "plugins": [ + "@typescript-eslint", + "sonarjs", + "prettier", + "jsdoc" + ], + "rules": { + "quotes": [ + "error", + "double" + ], + "semi": [ + "warn", + "always" + ], + "import/extensions": [ + "error", + "never" + ], + "no-await-in-loop": "error", + "no-constant-binary-expression": "error", + "no-constructor-return": "error", + "no-duplicate-imports": "error", + "no-self-compare": "error", + "no-template-curly-in-string": "error", + "no-unmodified-loop-condition": "warn", + "no-unreachable-loop": "warn", + "no-unused-private-class-members": "warn", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "off", + "camelcase": "off", + "dot-notation": "warn", + "func-style": [ + "error", + "declaration" + ], + "max-depth": [ + "warn", + 4 + ], + "max-lines": [ + "warn", + { + "max": 500, + "skipBlankLines": true, + "skipComments": true + } + ], + "max-lines-per-function": [ + "warn", + { + "max": 40, + "skipBlankLines": true, + "skipComments": true + } + ], + "no-confusing-arrow": "warn", + "no-console": "off", + "no-else-return": [ + "error", + { + "allowElseIf": true + } + ], + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-magic-numbers": [ + "warn", + { + "ignore": [ + 0, + 1 + ] + } + ], + "no-new": "warn", + "no-return-await": "error", + "no-sequences": "error", + "no-unused-expressions": "error", + "no-useless-computed-key": "warn", + "no-useless-concat": "error", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-call": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "no-with": "error", + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "warn", + "prefer-template": "error", + "require-await": "error", + "spaced-comment": "error", + "yoda": "error", + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "no-extra-parens": "off", + "no-whitespace-before-property": "error", + "no-trailing-spaces": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "object-curly-newline": [ + "error", + { + "ImportDeclaration": "never" + } + ], + "padded-blocks": [ + "error", + "never" + ], + "space-before-blocks": [ + "error", + "always" + ], + "space-before-function-paren": [ + "error", + "never" + ], + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": [ + "camelCase" + ] + }, + { + "selector": "variable", + "format": [ + "camelCase", + "UPPER_CASE" + ] + }, + { + "selector": "parameter", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "memberLike", + "modifiers": [ + "private" + ], + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "memberLike", + "format": [ + "camelCase", + "snake_case" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "variable", + "modifiers": [ + "destructured" + ], + "format": null + }, + { + "selector": "interface", + "format": [ + "PascalCase" + ], + "custom": { + "regex": "^I[A-Z]", + "match": false + } + } + ], + "@typescript-eslint/quotes": [ + "error", + "double" + ], + "no-underscore-dangle": "off", + "max-len": [ + "error", + { + "code": 80, + "ignorePattern": "(^import .*$|^export .*$)", + "ignoreStrings": true, + "ignoreComments": true, + "ignoreTemplateLiterals": true + } + ], + "operator-linebreak": "off", + "indent": "off", + "@typescript-eslint/indent": "off", + "no-empty-function": "off", + "implicit-arrow-linebreak": "off", + "function-paren-newline": "off", + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ], + "import/no-unresolved": "off", + "import/order": "off", + "class-methods-use-this": "off", + "brace-style": "off", + "@typescript-eslint/brace-style": "off", + "jsdoc/require-jsdoc": "off" + } +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3c99943 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "endOfLine": "lf", + "printWidth": 80, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "parser": "typescript" +} \ No newline at end of file diff --git a/package.json b/package.json index 1f0c936..8dbeb9e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ "command": "openproject.auth", "title": "Authorize", "shortTitle": "Auth" + }, + { + "command": "openproject.refresh", + "title": "Refresh work packages", + "shortTitle": "Refresh" } ], "viewsContainers": { @@ -43,10 +48,10 @@ "view": "openproject-workspaces", "type": "webview", "contents": "Hello! To use OpenProject features you need to authorize:\n[Auth](command:openproject.auth)", - "when": "!openproject.token" + "when": "!openproject.authed" } ], - "configuration":[ + "configuration": [ { "title": "OpenProject", "properties": { @@ -54,6 +59,11 @@ "type": "string", "default": null, "description": "OpenProject API token" + }, + "openproject.base_url": { + "type": "string", + "default": "https://board.dipal-local.ru", + "description": "OpenProject base_url" } } } @@ -67,22 +77,56 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile && npm run lint", "lint": "eslint src --ext ts", - "test": "node ./out/test/runTest.js" + "lint:fix": "eslint src --ext ts --fix", + "test": "jest" }, "devDependencies": { - "@types/vscode": "^1.78.0", + "@faker-js/faker": "^8.0.1", "@types/glob": "^8.1.0", + "@types/jest": "^29.5.1", "@types/mocha": "^10.0.1", "@types/node": "16.x", - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", - "eslint": "^8.39.0", + "@types/vscode": "^1.78.0", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", + "@vscode/test-electron": "^2.3.0", + "copy-webpack-plugin": "^11.0.0", + "eslint": "^8.41.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-airbnb": "^0.0.1-security", + "eslint-plugin-jsdoc": "^44.2.5", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-sonarjs": "^0.19.0", "glob": "^8.1.0", + "jest": "^29.5.0", "mocha": "^10.2.0", - "typescript": "^5.0.4", + "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", + "typescript": "^5.0.4", "webpack": "^5.81.0", - "webpack-cli": "^5.0.2", - "@vscode/test-electron": "^2.3.0" + "webpack-cli": "^5.0.2" + }, + "dependencies": { + "axios": "^1.4.0", + "client-oauth2": "^4.3.3", + "op-client": "^1.4.2" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage" } } From e97a647661a7c747a65621942339b2990f9eaae1 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:06:55 +0300 Subject: [PATCH 04/15] eslint rules updated --- .eslintrc.json | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3a78a61..0596d3e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,10 +15,21 @@ "overrides": [ { "files": [ - "**/*.spec.ts" + "**/*.spec.ts", + "**/*.mock.ts" ], "env": { "jest": true + }, + "rules": { + "import/first": "off", + "max-lines-per-function": "off", + "import/no-extraneous-dependencies": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/dot-notation": "off", + "sonarjs/no-duplicate-string": "off", + "@typescript-eslint/no-non-null-assertion": "off" } } ], @@ -61,7 +72,7 @@ "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off", "camelcase": "off", - "dot-notation": "warn", + "dot-notation": "off", "func-style": [ "error", "declaration" @@ -140,12 +151,7 @@ "error", "always" ], - "object-curly-newline": [ - "error", - { - "ImportDeclaration": "never" - } - ], + "object-curly-newline": "off", "padded-blocks": [ "error", "never" @@ -154,10 +160,7 @@ "error", "always" ], - "space-before-function-paren": [ - "error", - "never" - ], + "space-before-function-paren": "off", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/naming-convention": [ "error", @@ -255,6 +258,8 @@ "class-methods-use-this": "off", "brace-style": "off", "@typescript-eslint/brace-style": "off", - "jsdoc/require-jsdoc": "off" + "jsdoc/require-jsdoc": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "warn" } } \ No newline at end of file From b141efdc9ef8c533d5b8c9bb53a5869999a15620 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:07:03 +0300 Subject: [PATCH 05/15] gitignore updated --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 897cb9f..feaa83b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules package-lock.json -dist \ No newline at end of file +dist +out +coverage From 9b77ab956a863667af4564538b5bcfffd3d90e29 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:07:13 +0300 Subject: [PATCH 06/15] pretest removed --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8dbeb9e..1bcfc98 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "package": "webpack --mode production --devtool hidden-source-map", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", - "pretest": "npm run compile-tests && npm run compile && npm run lint", "lint": "eslint src --ext ts", "lint:fix": "eslint src --ext ts --fix", "test": "jest" From f847b30fd74df351ff3ebab8e4bb4fdb9530c7b5 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:07:49 +0300 Subject: [PATCH 07/15] webpack config formatted --- webpack.config.js | 60 +++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index b000c95..89b1e73 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,49 +1,53 @@ -'use strict'; +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable jsdoc/valid-types */ +/* eslint-disable spaced-comment */ +/* eslint-disable jsdoc/check-tag-names */ -const path = require('path'); +const path = require("path"); +const CopyPlugin = require("copy-webpack-plugin"); -/** @typedef {import('webpack').Configuration} WebpackConfig **/ +/** @typedef {import('webpack').Configuration} WebpackConfig */ /** @type WebpackConfig */ const extensionConfig = { - target: 'node', - mode: 'none', + target: "node", + mode: "none", - entry: './src/extension.ts', + entry: "./src/extension.ts", output: { - - path: path.resolve(__dirname, 'dist'), - filename: 'extension.js', - libraryTarget: 'commonjs2' + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", }, externals: { - vscode: 'commonjs vscode' + vscode: "commonjs vscode", }, resolve: { - extensions: ['.ts', '.js'] + extensions: [".ts", ".js"], }, module: { rules: [ { test: /\.ts$/, - exclude: /node_modules/, + exclude: /(node_modules|*\.spec\.ts)/, use: [ { - loader: 'ts-loader' - } - ] + loader: "ts-loader", + }, + ], }, { test: /resources\/*/, use: [ { - loader: 'file-loader' - } - ] - } - ] + loader: "file-loader", + }, + ], + }, + ], }, - devtool: 'nosources-source-map', + devtool: "nosources-source-map", infrastructureLogging: { level: "log", }, @@ -53,10 +57,10 @@ const extensionConfig = { { from: ".", to: "resources", - context: "resources" - } - ] - }) - ] + context: "resources", + }, + ], + }), + ], }; -module.exports = [ extensionConfig ]; \ No newline at end of file +module.exports = [extensionConfig]; From 5a099d743968151128e4f97e67184c7b246e8e99 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:07:59 +0300 Subject: [PATCH 08/15] tsconfig updated --- tsconfig.json | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 965a7b4..0205cec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,14 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "lib": [ - "ES2020" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } -} + "compilerOptions": { + "module": "commonjs", + "target": "ES2017", + "sourceMap": true, + "rootDir": "src", + "esModuleInterop": true, + "strict": true, + "types": [ + "jest", + "node" + ] + } +} \ No newline at end of file From 12587a1c88e60694ecd93d069c89d9386384bf6a Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:09:18 +0300 Subject: [PATCH 09/15] tree view --- resources/closed.png | Bin 0 -> 442 bytes resources/confirmed.png | Bin 0 -> 793 bytes resources/developed.png | Bin 0 -> 846 bytes resources/developing.png | Bin 0 -> 653 bytes resources/failed.png | Bin 0 -> 597 bytes resources/hold.png | Bin 0 -> 650 bytes resources/in_specification.png | Bin 0 -> 969 bytes resources/new.png | Bin 0 -> 629 bytes resources/rejected.png | Bin 0 -> 726 bytes resources/specified.png | Bin 0 -> 969 bytes resources/tested.png | Bin 0 -> 822 bytes resources/testing.png | Bin 0 -> 611 bytes src/__mocks__/vscode.js | 77 ++++ src/openProject.client.spec.ts | 108 +++++ src/openProject.client.ts | 65 +++ src/test/config.mock.ts | 9 + src/utils/getIconPathByStatus.util.spec.ts | 63 +++ src/utils/getIconPathByStatus.util.ts | 30 ++ .../openProject.treeDataProvider.spec.ts | 372 ++++++++++++++++++ src/views/openProject.treeDataProvider.ts | 73 ++++ 20 files changed, 797 insertions(+) create mode 100644 resources/closed.png create mode 100644 resources/confirmed.png create mode 100644 resources/developed.png create mode 100644 resources/developing.png create mode 100644 resources/failed.png create mode 100644 resources/hold.png create mode 100644 resources/in_specification.png create mode 100644 resources/new.png create mode 100644 resources/rejected.png create mode 100644 resources/specified.png create mode 100644 resources/tested.png create mode 100644 resources/testing.png create mode 100644 src/__mocks__/vscode.js create mode 100644 src/openProject.client.spec.ts create mode 100644 src/openProject.client.ts create mode 100644 src/test/config.mock.ts create mode 100644 src/utils/getIconPathByStatus.util.spec.ts create mode 100644 src/utils/getIconPathByStatus.util.ts create mode 100644 src/views/openProject.treeDataProvider.spec.ts create mode 100644 src/views/openProject.treeDataProvider.ts diff --git a/resources/closed.png b/resources/closed.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a5303563b168f8a618a232b44c5a8a3b78ed9d GIT binary patch literal 442 zcmV;r0Y(0aP);#9BAf>OM4*AG3| zA|%77lsemQ=kY5^8f$< literal 0 HcmV?d00001 diff --git a/resources/confirmed.png b/resources/confirmed.png new file mode 100644 index 0000000000000000000000000000000000000000..587271b9fc82b48f1c6b8a49a702a6ae1d90edc2 GIT binary patch literal 793 zcmV+!1LpjRP)sOfWdW`GU6S5U{<_BJ~Iz0h^n0im=#`sUSpbskQyeHdS+%jUw76y)dy9Wl!`W@qKP;hqsOJ2N`d=aT~;YJEq^iM;4I zASq~j(m6|k#qHe+7?M^gq#I(=#mZA@&W`63fkJ!F1bVGf08~K$l|DqRXdfR87#+3` zTudq2QQmH{igxGr%9?ZA0bu>~J$y7qr2#xa0MDb*e#e?;!@Ih@;Q)9)KN;EoZB+aX X$I|M6b$G`K00000NkvXXu0mjfWzS+h literal 0 HcmV?d00001 diff --git a/resources/developed.png b/resources/developed.png new file mode 100644 index 0000000000000000000000000000000000000000..92622c01a693d385d8ceb8e2cab9c5d688d7e575 GIT binary patch literal 846 zcmV-U1F`&xP)+Q&=bl3&A!4!d zA8>4OvEiAe+d*mRGcF-plk1U5WorRItJPcT8}5-xm3V#}IBLzh4UJZtlk|o%D`PAU zMq_2qzWv9Bi;7A*PoBOkT)NsqC8ZT^j^^9GL#xSe^XVifk0r% z&`@w@U~rHGLFg6%kme`HT-Z^RUf%B(b8iL$P%3!z!B4yHKWVl1ILduqk3J_iKf>N^ zMP7k6tfF#7*3{(Gs>f~je4_M8QMLK%nB{y>&bj;nB!tN!Rt!^3+skIV*SCa-+^=4B zHw^gu7Z=W35bpDNsOHp>F6ESjNO4Gr26GZ(UFPbgWUj8H8l9HTmKO@S%D5H)veC~{ zsnn;MTc40lSJnl9lZHxHWn^S|x%f!Qz%9QM1cK$WhfX}OH@CTaO#r@(Ht6kjUpKcu z6bk1pm?o3TRtJZIwH=nHNnng6nPw$+7>fXa YZ_l|mt^QFBxBvhE07*qoM6N<$f-PWyfB*mh literal 0 HcmV?d00001 diff --git a/resources/developing.png b/resources/developing.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2c7f126f21ae0272bb7b60489c590257bfc259 GIT binary patch literal 653 zcmV;80&@L{P)`|tUrf93c;HR z)~Qnkfz_cyhmz>pEq3b?D1jFtf}nyrL`G!Mp`cYdkPvNC!PRVU+j{r9P2Z=(RM;jO z^*#Nb-}C)_o-YqS_)kfjg*SBcsztFL+I7Hcvsvn0^T`j-ZUHEp7oK4F`mxKAw$1a; zxzBh*M*z@MivXBHkj|Sdy&gA!N`NGn%WUignRjD&^cfb_E?g&ukej=S)e%#%Np5sTY4_<2cSK2knqcAKFj~%ldJlGQpu%4RD9ybko-qk_*;&k)c$Ob? z?c^-sne*hM_iO&72H@&B$JZ4H-gJri6)VYKg8UN`xlm#~_F#8FQ||>91|AWa>gJ^~ z$d$7VyisJ@LniCK<2wV2dY0A93Kpe~hBJ4t1$!8K-$_=L82@+>XGd=hK&j~fxq`{> zf{FD&A4>OWmf~MF9fLv<$--=0f;f8btgYw%R{E9xG59hQxSGX=V_TCDaY67x?LU0)qd nB<7rdmAy*3UKstCxeEOON+Z3OCd@?V00000NkvXXu0mjf_oE}& literal 0 HcmV?d00001 diff --git a/resources/failed.png b/resources/failed.png new file mode 100644 index 0000000000000000000000000000000000000000..93852950c422c3b01715c1161562a978f74f2f21 GIT binary patch literal 597 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH}&m zoe#o{9~a$m1~Mc|Tq8ARU0+B@Pe@2tNJtxq1O;8CrM39^%|%6xL_`u))bvF}fC_-f zOIFTYRLoOW)NS+T2j!{vGRaK2tQHfSj30G2zP*Mt2RID;E zEY;J?($>z_*8ZQ8y4Bs|grEOO|A6^+cI#YRv%H}~J2F=J?$wQ@`J(k=3GQ$8g8SEzh>)Fw`1{yY`WzDZmSAATsrOe#6) z$q~SQu6C)klfpywZEYt2sNjEXnmgtuWL5`p%#k+oo*O^f3F^ z`s?_nn-$ZP7QLQ-L8RvJ{3ub5bNdc2lVV7DKL6$AIZO;4SGMs_+P-hup8vldKlR%@ dBZ%<<>*Uuu;U@oBZvvgp;OXk;vd$@?2>^t=>+%2q literal 0 HcmV?d00001 diff --git a/resources/hold.png b/resources/hold.png new file mode 100644 index 0000000000000000000000000000000000000000..4187bddaf3c067d21dd1951571c2c9478b7c2b6f GIT binary patch literal 650 zcmV;50(Jd~P)5@ppU9>DYKophTvW@N=_=6WOud2|p)(0`4H22aY2mfKdRU{}Rvw3{g$$ zMH;M zghL22kK{}OhoTIQ^S2&127p#u6l5lDDZr~DdHu13#LFd2Wkx_L={B2f=h1wj>gNA> z1t}!r_PHYnwtvM8eZ&g~ru8Z*RW)YTj_q5cq@<51%lGx1S!?LwkZDDN@~07*qoM6N<$f`7poA^-pY literal 0 HcmV?d00001 diff --git a/resources/in_specification.png b/resources/in_specification.png new file mode 100644 index 0000000000000000000000000000000000000000..8329632dc3781f05d824680dc614f3d79aa5e6f6 GIT binary patch literal 969 zcmV;)12+7LP)+EO-PEZZETOdV`v z)1{ywA!f*+AetBxT?`jWkVw2hE*v)yz9blki3Gi{kt}iev4O)CW@aXmkm=}#jI&wh zv}dQMJ?-h*0F+YjyloLcuh#^03gnZzUG!TgUP}{ozE>iRs-U3Ndu710|)>#JiY$2zyFFJnV6Wkmv3y|y8V#T zxwNCQx<;~cd}b&-Q0j7eYK8`{-~4f)zYRbR0GN)A&4=t(OCYAFTQibW{cv`68UWId zBEQ>RIahLAsE9M6Ka~w=Kc9pa^~$OKj__S(4{0rSDYi8SGdbugCoDHQ}?IC(WE?FUS7Fw zrLS)P@NZ+>z{nkvB2I_NY`!H-Pd;>3I-ZTj#kDRAt4lI+glT`my{6k)#mglsPA14f zF_Q}OX8!ea-IuE=T}Ue)PdeUuKX{1)N5+0jECfsi&-tApj7HjH1a^ z=6PjB^%XzSoTM7J49kE#2)_A~x0rh>CP~=R z_m#52yL^^m7(+M~1!3DP?Wu r`*@5fz{*)=nYqd_PAKTiqqy~7ZfQDG?;1!T00000NkvXXu0mjf3Ps0j literal 0 HcmV?d00001 diff --git a/resources/new.png b/resources/new.png new file mode 100644 index 0000000000000000000000000000000000000000..8870415f2e83fc67a49b441843946b573132c308 GIT binary patch literal 629 zcmV-*0*d{KP)P@wOo*Jrom1c*#h25Tb*f zC1@bCE>VZB9(U^&UFs$af487xkE6H;47c1jfL#<%steVnwt_%1 zFy!dYTVTA?R)5)+=j4rYv|I&{0KTLz;ONeaGn4_YxX14E&~nK8Pat&h5V(1Ua+BLf z&QT0N_>#VW)MXa1yAU7{JAJjjYq~JRC0y+2aFp2#v*aszERRLd6J)Ejg&_^5L(}Ab z=CC{#gS~@ne&1x}>&oSNZ+$>YSB?YN=WSMZS4n(I5cY>z{IJOU$ULLIQ8J|r>wD`= zm=kP#+aO&?1E8b|>j*T|pS9-fIS!5wFr-0pCP})G#`0LGW&lo~HO;tX7y@zM_S&rN zt&#naWvqXUcs5QX6ru2|z*J}o(=a(~95QK5B84Ph$s;ux7v5!WNOQDp6hqxSBJi=N z(Q{SSqP3DjCAutw;%V`2u`U5z$6W&6by0t#p{1bZD_gIviw(E&9vJywQy_K}YNi^i zmQM(pn6J!73iSXOXc@Z#Q3M`WVwGnng&iL{58dWN*@1+iy}GSPLBdnVeXML{v2CnaAnt=_0}yMRC(j+*SKn$d@NH4xC2loPTI5)i3c!j6 zg9|#zApx3>BxaU$53n8J`@1@9<*(Kao*lbzKPkYSk1nFx^jdvpS@Q^& zN9%SFQ3!B*x((j80pMEDnXPK&{eco}r_LhoWRb0?WQTOnKd*n5ol|P%FQ3k4c`;zI zSU7b4GWLcW>{rwc^>Lq$b5eaI0o+d!%`eiG!p&Rwad{gyX%4fp@m>c?2LkP{$JFyd zMtN>iTxC{~wv|<``KgXq3H@g>y%7q;ux5DcULHvj+t07*qo IM6N<$g4FTXh)5pYuW8>00Po+MaRR1F}L%8C(DvsELfF z!;i!boyMujvP`qMcq11Y)CfUwW-jXPG)tU`&Lw6M7Z*Pgzmmzaxp{$PFe%bPdrI5W zp40DhEVTW5AuJ;#zR7#@KEFJ9p5&zfpslT~zs=V<-r#cc6h$%kpCN=)q)0fv7zm8d z&i?r;D~h7;;1h?xc;)pm;cjp-Fo(&{-UC_7;n4FF*wyO0TlSuQcjBVaxZRJv1OD@J zu0m1;vT4UgUnN!PQ=-qGA!8%Qx9BVBGI{#BV^gffYTH56^p>bXqZhXqWvu2?5Slx; zRsjXG*=%EDk`z!?HzM4P5|$A1+^dcAcY>N%=~tr2Ph<2mmwy2!ha>Rx-cH&^8wYm{Mt!b4Po_QQh9S2fFZ&c;X zj8*Hj?|~dEuWeK?Ey#-DRyeHR3@+(nd!Gvc5*e+0@uw?_oXM4zlKBh(@Xa>I)tTFw z`Q^y!LM)L7U}AhddoR3n`s+c1Z=j}U=!8L9`ui}GSCVhOd}@4f|Iym^j{U6NCU(n8 zBH!9w|Nh9?BOE)n^rLAMJE`Ou^Zk;KdeXk`jy-n}K1NUQI7<6L>cS-VIGJ zFd)?d-d%4=NGil#P6Vbyu<{ls?SJaTTyiA@04QWtlnOcOzV801L@Ivg^Q71^%w|i@ z+aa#y(nSD3ESv%Wu%ApEZ)I46Qp~F9L^NoW<5Jz69NxXt=~6RURRw_Gr>{0-vufd^ riJ7%Vk8|bv&$qOh8$ml~=F literal 0 HcmV?d00001 diff --git a/resources/tested.png b/resources/tested.png new file mode 100644 index 0000000000000000000000000000000000000000..433cd9e8bbbcbfedbaac030d65baed4fbe5ca7a8 GIT binary patch literal 822 zcmV-61Ihe}P)A(IY<&Ocp6DgLWg2aYYsCML*aCyC5+(W-+03{DJj zrt3UKg+7XWK33H?0FX>3=jH&hSd5)Ne@$xf9Pog$U^b=KV*reX3fb%F0SAhIP4i=A zh)^hmZQBLn>Gt;1-;l1Gk0>vu-12ffu$k_yt#of~<;#Nglsue7loyk(4bp!01X9kN zmjet>jU%lrGM3_C**mOxqngJ? zUtYowD?dWhG+ZRM21eA#eXB4GV>BEN%U$0eCR00~$5o|V>Fwdg^ls zdw7P4WdYpLj2zqE@v$7Aya)^_&*Vq(HdS5KYF>CnuUS+QsF>$*?hcX4Sd3Q|;wCNI zd0>t?f5iK;m;PD{OfwVeJk0_Gs;Kt0tm!P=@M^h4GSziAZna(8$h6xBxWH5{(*FWD zfQ^)KIbxMfk6M2Fw`k(=<@={yGyN@K|Lw`?3HGQw?+1w_82|tP07*qoM6N<$f?QRF A{r~^~ literal 0 HcmV?d00001 diff --git a/resources/testing.png b/resources/testing.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2cc654a9c01efc29cdcfbb3d347480a9b4f081 GIT binary patch literal 611 zcmV-p0-XJcP)Hlu46vzX!fGnSR@Up+uj7)21DpoP$;tiJ)YK&30Ac}c zydWzp8&+Fe%Lwy=v9U3af`URgQ3fzFGKwoGC`8%W*|EaJG&MC@Sy@@r7#SHQ(A8t4 z134+-*^c%$5-(o7VA!>N!`Jf%*M8KI`pZ!rz{%aVoM#3D1495#1Gw4POd_1c&DH*F zXI$2?=1ru%m{56%JU2}K{GCr0Tx@LS-+ui%33oAaQr42^J9Bcfr7Lts|z`($8>E36CKY#zjZD(L$U@%nSV_;xlU`X+l@Q{qLj{nEY93@_gQWY~WCHA88bBEx@1gvIbMP?qL&5#i8I$3G`E2+05d002ovPDHLkV1mb18|VN4 literal 0 HcmV?d00001 diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js new file mode 100644 index 0000000..e406dc3 --- /dev/null +++ b/src/__mocks__/vscode.js @@ -0,0 +1,77 @@ +/* eslint-disable max-classes-per-file */ +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/naming-convention */ + +class Disposable {} + +class EventEmitter {} + +const languages = { + createDiagnosticCollection: jest.fn(), +}; + +const StatusBarAlignment = {}; + +const window = { + createStatusBarItem: jest.fn(() => ({ + show: jest.fn(), + })), + showErrorMessage: jest.fn(), + showWarningMessage: jest.fn(), + showInformationMessage: jest.fn(), + createTextEditorDecorationType: jest.fn(), + createTreeView: jest.fn(), +}; + +const workspace = { + getConfiguration: jest.fn(), + workspaceFolders: [], + onDidSaveTextDocument: jest.fn(), +}; + +const OverviewRulerLane = { + Left: null, +}; + +const Uri = { + file: (f) => f, + parse: jest.fn(), +}; +const Range = jest.fn(); +const Diagnostic = jest.fn(); +const DiagnosticSeverity = { Error: 0, Warning: 1, Information: 2, Hint: 3 }; + +const debug = { + onDidTerminateDebugSession: jest.fn(), + startDebugging: jest.fn(), +}; + +const commands = { + executeCommand: jest.fn(), + registerCommand: jest.fn(), +}; + +const TreeItemCollapsibleState = { + None: 0, + Collapsed: 1, + Expanded: 2, +}; + +const vscode = { + TreeItemCollapsibleState, + EventEmitter, + Disposable, + languages, + StatusBarAlignment, + window, + workspace, + OverviewRulerLane, + Uri, + Range, + Diagnostic, + DiagnosticSeverity, + debug, + commands, +}; + +module.exports = vscode; diff --git a/src/openProject.client.spec.ts b/src/openProject.client.spec.ts new file mode 100644 index 0000000..7e98421 --- /dev/null +++ b/src/openProject.client.spec.ts @@ -0,0 +1,108 @@ +import { faker } from "@faker-js/faker"; +import { User, WP } from "op-client"; +import OpenProjectClient, { + addTokenToUrl, + createLogger, +} from "./openProject.client"; + +jest.mock("op-client"); + +describe("OpenProject Client tests", () => { + let client: OpenProjectClient; + + beforeEach(() => { + jest.clearAllMocks(); + client = OpenProjectClient.getInstance(); + client["entityManager"] = { + fetch: jest.fn(), + get: jest.fn(), + getMany: jest.fn(), + } as any; + }); + + describe("Initialization", () => { + it("should be initialized correctly", async () => { + const token = faker.string.alphanumeric(); + const baseUrl = faker.internet.url(); + const user = new User(1); + + jest.spyOn(client, "getUser").mockResolvedValueOnce(user); + + expect(await client.init(baseUrl, token)).toEqual(user); + }); + }); + + describe("GetUser", () => { + it("should call entity manager's fetch", async () => { + jest.spyOn(client["entityManager"]!, "fetch").mockResolvedValueOnce(1); + + await client.getUser(); + + expect(client["entityManager"]!.fetch).toHaveBeenCalledWith( + "api/v3/users/me", + ); + }); + it("should return user", async () => { + const user = new User(1); + + jest.spyOn(client["entityManager"]!, "fetch").mockResolvedValueOnce(user); + + expect(await client.getUser()).toEqual(user); + }); + it("should return undefined if entity manager is null", async () => { + client["entityManager"] = undefined; + + expect(await client.getUser()).toBeUndefined(); + }); + it("should return undefined if error returned", async () => { + jest.spyOn(client["entityManager"]!, "fetch").mockRejectedValueOnce(null); + + expect(await client.getUser()).toBeUndefined(); + }); + }); + + describe("getWPs", () => { + it("should call getMany", async () => { + jest.spyOn(client["entityManager"]!, "getMany").mockResolvedValueOnce([]); + + await client.getWPs(); + + expect(client["entityManager"]!.getMany).toHaveBeenLastCalledWith(WP, { + pageSize: 100, + all: true, + filters: [], + }); + }); + it("should return wps", async () => { + const wps = faker.helpers.uniqueArray(() => new WP(1), 5); + + jest + .spyOn(client["entityManager"]!, "getMany") + .mockResolvedValueOnce(wps); + + expect(await client.getWPs()).toEqual(wps); + }); + it("should return undefined if entity manager is null", async () => { + client["entityManager"] = undefined; + + expect(await client.getWPs()).toBeUndefined(); + }); + }); +}); + +describe("addTokenToUrl", () => { + it("should return correct url", () => { + const token = faker.string.alphanumeric(10); + const url = "http://google.com"; + + const result = `http://apikey:${token}@google.com/`; + + expect(addTokenToUrl(url, token)).toBe(result); + }); +}); + +describe("createLogger", () => { + it("should return console", () => { + expect(createLogger()).toEqual(console); + }); +}); diff --git a/src/openProject.client.ts b/src/openProject.client.ts new file mode 100644 index 0000000..6b66b9d --- /dev/null +++ b/src/openProject.client.ts @@ -0,0 +1,65 @@ +import { EntityManager, User, WP } from "op-client"; +import { URL } from "url"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/naming-convention, import/no-extraneous-dependencies +const ClientOAuth2 = require("client-oauth2"); + +export default class OpenProjectClient { + private static _instance: OpenProjectClient; + + public static getInstance(): OpenProjectClient { + if (!this._instance) { + this._instance = new OpenProjectClient(); + } + return this._instance; + } + + private entityManager?: EntityManager; + + public init( + baseUrl: string, + token: string, + ): Promise | undefined { + this.entityManager = new EntityManager({ + baseUrl: addTokenToUrl(baseUrl, token), + createLogger, + token: new ClientOAuth2({}).createToken(token, {}), + }); + return this.getUser(); + } + + public getUser(): Promise | undefined { + return this.entityManager + ?.fetch("api/v3/users/me") + .then((response) => new User(response)) + .catch((err) => { + console.error(err); + return undefined; + }); + } + + public getWPs(): Promise | undefined { + return this.entityManager?.getMany(WP, { + pageSize: 100, + all: true, + filters: [], + }); + } +} + +/** + * makes http://apikey:*token*@google.com out of http://google.com + * @param url url to which we want to add token + * @param token api token + * @returns url with token + */ +export function addTokenToUrl(url: string, token: string): string { + const parsedUrl = new URL(url); + parsedUrl.username = "apikey"; + parsedUrl.password = token; + return parsedUrl.toString(); +} + +export function createLogger() { + return console; +} diff --git a/src/test/config.mock.ts b/src/test/config.mock.ts new file mode 100644 index 0000000..7d39476 --- /dev/null +++ b/src/test/config.mock.ts @@ -0,0 +1,9 @@ +class VSCodeConfigMock { + constructor(private readonly _data: { [key: string]: any }) {} + + get(key: string) { + return this._data[key]; + } +} + +export default VSCodeConfigMock; diff --git a/src/utils/getIconPathByStatus.util.spec.ts b/src/utils/getIconPathByStatus.util.spec.ts new file mode 100644 index 0000000..3404a11 --- /dev/null +++ b/src/utils/getIconPathByStatus.util.spec.ts @@ -0,0 +1,63 @@ +import { faker } from "@faker-js/faker"; +import getIconPathByStatus from "./getIconPathByStatus.util"; + +describe("getIconPathByStatus test suit", () => { + describe("return correct path suit", () => { + it("should return correct path of 'Confirmed' icon", () => { + expect(getIconPathByStatus("Confirmed")).toEqual( + "resources/confirmed.png", + ); + }); + it("should return correct path of 'In specification' icon", () => { + expect(getIconPathByStatus("In specification")).toEqual( + "resources/in_specification.png", + ); + }); + it("should return correct path of 'Specified' icon", () => { + expect(getIconPathByStatus("Specified")).toEqual( + "resources/specified.png", + ); + }); + it("should return correct path of 'In progress' icon", () => { + expect(getIconPathByStatus("In progress")).toEqual( + "resources/developing.png", + ); + }); + it("should return correct path of 'Developed' icon", () => { + expect(getIconPathByStatus("Developed")).toEqual( + "resources/developed.png", + ); + }); + it("should return correct path of 'In testing' icon", () => { + expect(getIconPathByStatus("In testing")).toEqual( + "resources/testing.png", + ); + }); + it("should return correct path of 'Tested' icon", () => { + expect(getIconPathByStatus("Tested")).toEqual("resources/tested.png"); + }); + it("should return correct path of 'Test failed' icon", () => { + expect(getIconPathByStatus("Test failed")).toEqual( + "resources/failed.png", + ); + }); + it("should return correct path of 'On hold' icon", () => { + expect(getIconPathByStatus("On hold")).toEqual("resources/hold.png"); + }); + it("should return correct path of 'Closed' icon", () => { + expect(getIconPathByStatus("Closed")).toEqual("resources/closed.png"); + }); + it("should return correct path of 'Rejected' icon", () => { + expect(getIconPathByStatus("Rejected")).toEqual("resources/rejected.png"); + }); + it("should return undefined for new", () => { + expect(getIconPathByStatus("New")).toEqual(undefined); + }); + }); + + describe("Others should get undefined", () => { + it("should return undefined", () => { + expect(getIconPathByStatus(faker.string.alpha())).toEqual(undefined); + }); + }); +}); diff --git a/src/utils/getIconPathByStatus.util.ts b/src/utils/getIconPathByStatus.util.ts new file mode 100644 index 0000000..a96872c --- /dev/null +++ b/src/utils/getIconPathByStatus.util.ts @@ -0,0 +1,30 @@ +export default function getIconPathByStatus( + status?: string, +): string | undefined { + switch (status) { + case "Confirmed": + return "resources/confirmed.png"; + case "In specification": + return "resources/in_specification.png"; + case "Specified": + return "resources/specified.png"; + case "In progress": + return "resources/developing.png"; + case "Developed": + return "resources/developed.png"; + case "In testing": + return "resources/testing.png"; + case "Tested": + return "resources/tested.png"; + case "Test failed": + return "resources/failed.png"; + case "On hold": + return "resources/hold.png"; + case "Closed": + return "resources/closed.png"; + case "Rejected": + return "resources/rejected.png"; + default: + return undefined; + } +} diff --git a/src/views/openProject.treeDataProvider.spec.ts b/src/views/openProject.treeDataProvider.spec.ts new file mode 100644 index 0000000..37c0d5d --- /dev/null +++ b/src/views/openProject.treeDataProvider.spec.ts @@ -0,0 +1,372 @@ +import { TreeItemCollapsibleState } from "vscode"; +import OpenProjectTreeDataProvider from "./openProject.treeDataProvider"; + +describe("OpenProjectTreeDataProvider", () => { + let instance: OpenProjectTreeDataProvider; + + beforeEach(() => { + instance = OpenProjectTreeDataProvider.getInstance(); + jest.spyOn(instance["_client"], "getWPs").mockResolvedValue([]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("refreshWPs", () => { + it("should update the workPackages array with new work packages", async () => { + instance["_onDidChangeTreeData"] = { fire: jest.fn() } as any; + jest.spyOn(instance["_client"], "getWPs").mockResolvedValue([ + { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + } as any, + ]); + + await instance.refreshWPs(); + + expect(instance["workPackages"]).toEqual([ + { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }, + ]); + }); + it("should fire _onDidChangeTreeData", async () => { + instance["_onDidChangeTreeData"] = { fire: jest.fn() } as any; + jest.spyOn(instance["_client"], "getWPs").mockResolvedValue([ + { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + } as any, + ]); + + await instance.refreshWPs(); + + expect(instance["_onDidChangeTreeData"].fire).toHaveBeenCalled(); + }); + }); + + describe("getTreeItem", () => { + it("should return a tree item with label, collapsible state and icon path", async () => { + const wp = { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }; + const treeItem = await instance.getTreeItem(wp as any); + expect(treeItem.label).toEqual("#1 Test Work Package"); + expect(treeItem.collapsibleState).toEqual(TreeItemCollapsibleState.None); + expect(treeItem.iconPath).toBeUndefined(); + }); + it("should return a tree item with label, collapsible state and icon path", async () => { + const wp = { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: null, + parent: null, + ancestor: null, + }; + const treeItem = await instance.getTreeItem(wp as any); + expect(treeItem.label).toEqual("#1 Test Work Package"); + expect(treeItem.collapsibleState).toEqual(TreeItemCollapsibleState.None); + expect(treeItem.iconPath).toBeUndefined(); + }); + it("should return a tree item with label, collapsible state = collapsed and icon path", async () => { + const wp = { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "New", + }, + }, + children: [{}], + parent: null, + ancestor: null, + }; + const treeItem = await instance.getTreeItem(wp as any); + expect(treeItem.label).toEqual("#1 Test Work Package"); + expect(treeItem.collapsibleState).toEqual( + TreeItemCollapsibleState.Collapsed, + ); + expect(treeItem.iconPath).toBeUndefined(); + }); + it("should return a tree item with label, collapsible state and some icon path", async () => { + const wp = { + id: 1, + subject: "Test Work Package", + status: { + self: { + title: "In progress", + }, + }, + children: [{}], + parent: null, + ancestor: null, + }; + const treeItem = await instance.getTreeItem(wp as any); + expect(treeItem.label).toEqual("#1 Test Work Package"); + expect(treeItem.collapsibleState).toEqual( + TreeItemCollapsibleState.Collapsed, + ); + expect(treeItem.iconPath).not.toBeUndefined(); + }); + }); + + describe("getChildren", () => { + it("should return the top-level work packages", () => { + instance["workPackages"] = [ + { + id: 1, + subject: "Test Work Package 1", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }, + { + id: 2, + subject: "Test Work Package 2", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }, + ] as any; + const topLevelWPs = instance.getChildren(); + expect(topLevelWPs).toEqual([ + { + id: 1, + subject: "Test Work Package 1", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }, + { + id: 2, + subject: "Test Work Package 2", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: null, + ancestor: null, + }, + ]); + }); + + it("should return the children of a work package", () => { + const parentWP = { + id: 1, + subject: "Parent Work Package", + status: { + self: { + title: "New", + }, + }, + children: [ + { + id: 2, + subject: "Child Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: { + id: 1, + subject: "Parent Work Package", + }, + ancestor: null, + }, + ], + parent: null, + ancestor: null, + }; + instance["workPackages"] = [parentWP, ...parentWP.children] as any; + + expect(instance.getChildren(parentWP as any)).toEqual([ + { + id: 2, + subject: "Child Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: { + id: 1, + subject: "Parent Work Package", + }, + ancestor: null, + }, + ]); + }); + }); + + describe("resolveTreeItem", () => { + it("should return item", () => { + const item = {}; + expect(instance.resolveTreeItem(item)).toEqual(item); + }); + }); + + describe("getParent", () => { + it("should return the ancestor of a work package", () => { + const ancestorWP = { + id: 1, + subject: "Ancestor Work Package", + status: { + self: { + title: "New", + }, + }, + children: [ + { + id: 2, + subject: "Parent Work Package", + status: { + self: { + title: "New", + }, + }, + children: [ + { + id: 3, + subject: "Child Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: { + id: 2, + subject: "Parent Work Package", + }, + ancestor: { + id: 1, + subject: "Ancestor Work Package", + }, + }, + ], + parent: null, + ancestor: { + id: 1, + subject: "Ancestor Work Package", + }, + }, + ], + parent: null, + ancestor: null, + }; + instance["workPackages"] = [ancestorWP, ...ancestorWP.children] as any; + const parentWP = instance.getParent({ + id: 3, + subject: "Child Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: { + id: 2, + subject: "Parent Work Package", + }, + ancestor: { + id: 1, + subject: "Ancestor Work Package", + }, + } as any); + expect(parentWP).toEqual({ + id: 2, + subject: "Parent Work Package", + status: { + self: { + title: "New", + }, + }, + children: [ + { + id: 3, + subject: "Child Work Package", + status: { + self: { + title: "New", + }, + }, + children: [], + parent: { + id: 2, + subject: "Parent Work Package", + }, + ancestor: { + id: 1, + subject: "Ancestor Work Package", + }, + }, + ], + parent: null, + ancestor: { + id: 1, + subject: "Ancestor Work Package", + }, + }); + }); + }); +}); diff --git a/src/views/openProject.treeDataProvider.ts b/src/views/openProject.treeDataProvider.ts new file mode 100644 index 0000000..e5fbe43 --- /dev/null +++ b/src/views/openProject.treeDataProvider.ts @@ -0,0 +1,73 @@ +import { WP } from "op-client"; +import * as vscode from "vscode"; +import { Event, ProviderResult, TreeDataProvider, TreeItem } from "vscode"; +import OpenProjectClient from "../openProject.client"; +import getIconPathByStatus from "../utils/getIconPathByStatus.util"; +import path from "path"; + +export default class OpenProjectTreeDataProvider + implements TreeDataProvider +{ + private static _instance: OpenProjectTreeDataProvider; + + public static getInstance(): OpenProjectTreeDataProvider { + if (!this._instance) { + this._instance = new OpenProjectTreeDataProvider(); + } + return this._instance; + } + + private _client: OpenProjectClient; + + private workPackages: WP[] = []; + + private _onDidChangeTreeData: vscode.EventEmitter< + void | WP | WP[] | null | undefined + > = new vscode.EventEmitter(); + + private constructor() { + this._client = OpenProjectClient.getInstance(); + this.refreshWPs(); + } + + onDidChangeTreeData?: Event = + this._onDidChangeTreeData.event; + + getTreeItem(element: WP): TreeItem | Promise { + const iconPath = getIconPathByStatus(element.status.self.title); + return { + label: `#${element.id} ${element.subject}`, + collapsibleState: + element.children?.length > 0 + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None, + iconPath: iconPath + ? vscode.Uri.file(path.join(__dirname, iconPath)) + : undefined, + }; + } + + getChildren(element?: WP | undefined): ProviderResult { + if (!element) { + return this.workPackages.filter((wp) => !wp.parent); + } + return this.workPackages.filter((wp) => wp.parent?.id === element.id); + } + + getParent(element: WP): ProviderResult { + return this.workPackages.find((wp) => wp.id === element.parent.id); + } + + resolveTreeItem(item: TreeItem): ProviderResult { + return item; + } + + refreshWPs(): Promise | undefined { + return this._client.getWPs()?.then((wps) => { + if (wps.length) { + this.workPackages = wps; + this._onDidChangeTreeData.fire(); + } + }); + } +} From 8ace9446f94afea448f3ed3c821cb63ec9c07a71 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:10:47 +0300 Subject: [PATCH 10/15] commands, auth, refresh work packages, tree view registered --- src/commands/authorizeClient.command.spec.ts | 110 +++++++++++++++++++ src/commands/authorizeClient.command.ts | 21 ++++ src/commands/refreshWPs.command.spec.ts | 20 ++++ src/commands/refreshWPs.command.ts | 5 + src/extension.spec.ts | 52 +++++++++ src/extension.ts | 30 +++-- 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 src/commands/authorizeClient.command.spec.ts create mode 100644 src/commands/authorizeClient.command.ts create mode 100644 src/commands/refreshWPs.command.spec.ts create mode 100644 src/commands/refreshWPs.command.ts create mode 100644 src/extension.spec.ts diff --git a/src/commands/authorizeClient.command.spec.ts b/src/commands/authorizeClient.command.spec.ts new file mode 100644 index 0000000..613d609 --- /dev/null +++ b/src/commands/authorizeClient.command.spec.ts @@ -0,0 +1,110 @@ +// jest.mock("../__mocks__/vscode", () => vscode); +jest.mock("../openProject.client"); +jest.mock("../views/openProject.treeDataProvider"); +const vscode = require("../__mocks__/vscode"); + +import { faker } from "@faker-js/faker"; +import { User } from "op-client"; +import OpenProjectClient from "../openProject.client"; +import VSCodeConfigMock from "../test/config.mock"; +import OpenProjectTreeDataProvider from "../views/openProject.treeDataProvider"; +import authorizeClient from "./authorizeClient.command"; + +describe("Authorize client command test suit", () => { + const client = new OpenProjectClient(); + const config = new VSCodeConfigMock({ + base_url: faker.internet.url(), + token: faker.string.sample(), + }); + const user = new User(1); + const treeDataProvider = { refreshWPs: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(OpenProjectClient, "getInstance").mockReturnValue(client); + jest.spyOn(vscode.workspace, "getConfiguration").mockReturnValue(config); + jest.spyOn(client, "init").mockResolvedValue(user); + + jest + .spyOn(OpenProjectTreeDataProvider, "getInstance") + .mockReturnValue(treeDataProvider as any); + + user.firstName = faker.person.firstName(); + user.lastName = faker.person.lastName(); + }); + + describe("Init call", () => { + it("should call init with correct data", async () => { + await authorizeClient(); + expect(client.init).toHaveBeenLastCalledWith( + config.get("base_url"), + config.get("token"), + ); + }); + it("should call init empty string", async () => { + const emptyConfig = new VSCodeConfigMock({}); + jest + .spyOn(vscode.workspace, "getConfiguration") + .mockReturnValue(emptyConfig); + + await authorizeClient(); + + expect(client.init).toHaveBeenLastCalledWith("", ""); + }); + }); + + describe("On success", () => { + it("should set 'openproject.authed' to true on success", async () => { + jest.spyOn(vscode.commands, "executeCommand"); + + await authorizeClient(); + + expect(vscode.commands.executeCommand).toHaveBeenLastCalledWith( + "setContext", + "openproject.authed", + true, + ); + }); + + it("should show message 'Hello' on success", async () => { + jest.spyOn(vscode.window, "showInformationMessage"); + + await authorizeClient(); + + expect(vscode.window.showInformationMessage).toHaveBeenLastCalledWith( + `Hello, ${user.firstName} ${user.lastName}!`, + ); + }); + + it("should call 'refresh WPs' on treeDataProvider", async () => { + await authorizeClient(); + + expect(treeDataProvider.refreshWPs).toHaveBeenCalled(); + }); + }); + describe("On fail", () => { + it("should show error message", async () => { + jest.spyOn(vscode.window, "showErrorMessage"); + jest.spyOn(client, "init").mockResolvedValue(undefined); + + await authorizeClient(); + + expect(vscode.window.showErrorMessage).toHaveBeenLastCalledWith( + "Failed connecting to OpenProject", + ); + }); + + it("should call nothing else", async () => { + jest.spyOn(client, "init").mockResolvedValue(undefined); + jest.spyOn(OpenProjectTreeDataProvider, "getInstance"); + jest.spyOn(vscode.commands, "executeCommand"); + jest.spyOn(vscode.window, "showInformationMessage"); + + await authorizeClient(); + + expect(OpenProjectTreeDataProvider.getInstance).not.toHaveBeenCalled(); + expect(vscode.commands.executeCommand).not.toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/commands/authorizeClient.command.ts b/src/commands/authorizeClient.command.ts new file mode 100644 index 0000000..c777c89 --- /dev/null +++ b/src/commands/authorizeClient.command.ts @@ -0,0 +1,21 @@ +import OpenProjectClient from "../openProject.client"; +import * as vscode from "vscode"; +import OpenProjectTreeDataProvider from "../views/openProject.treeDataProvider"; + +export default async function authorizeClient() { + const config = vscode.workspace.getConfiguration("openproject"); + const client = OpenProjectClient.getInstance(); + const user = await client.init( + config.get("base_url") ?? "", + config.get("token") ?? "", + ); + if (!user) { + vscode.window.showErrorMessage("Failed connecting to OpenProject"); + return; + } + vscode.commands.executeCommand("setContext", "openproject.authed", true); + vscode.window.showInformationMessage( + `Hello, ${user.firstName} ${user.lastName}!`, + ); + OpenProjectTreeDataProvider.getInstance().refreshWPs(); +} diff --git a/src/commands/refreshWPs.command.spec.ts b/src/commands/refreshWPs.command.spec.ts new file mode 100644 index 0000000..061a411 --- /dev/null +++ b/src/commands/refreshWPs.command.spec.ts @@ -0,0 +1,20 @@ +jest.mock("../views/openProject.treeDataProvider"); + +import OpenProjectTreeDataProvider from "../views/openProject.treeDataProvider"; +import refreshWPs from "./refreshWPs.command"; + +describe("refresh WPs command test suit", () => { + it("should call refreshWPs func", () => { + const treeDataProvider = { refreshWPs: jest.fn() }; + + jest + .spyOn(OpenProjectTreeDataProvider, "getInstance") + .mockReturnValue(treeDataProvider as any); + + jest.spyOn(treeDataProvider, "refreshWPs"); + + refreshWPs(); + + expect(treeDataProvider.refreshWPs).toHaveBeenCalled(); + }); +}); diff --git a/src/commands/refreshWPs.command.ts b/src/commands/refreshWPs.command.ts new file mode 100644 index 0000000..d076de6 --- /dev/null +++ b/src/commands/refreshWPs.command.ts @@ -0,0 +1,5 @@ +import OpenProjectTreeDataProvider from "../views/openProject.treeDataProvider"; + +export default function refreshWPs() { + OpenProjectTreeDataProvider.getInstance().refreshWPs(); +} diff --git a/src/extension.spec.ts b/src/extension.spec.ts new file mode 100644 index 0000000..0e473d7 --- /dev/null +++ b/src/extension.spec.ts @@ -0,0 +1,52 @@ +const vscode = require("./__mocks__/vscode"); + +jest.mock("./views/openProject.treeDataProvider"); +jest.mock("./commands/authorizeClient.command"); +jest.mock("./commands/refreshWPs.command"); + +import OpenProjectTreeDataProvider from "./views/openProject.treeDataProvider"; +import authorizeClient from "./commands/authorizeClient.command"; +import refreshWPs from "./commands/refreshWPs.command"; +import { activate, deactivate } from "./extension"; + +describe("activate", () => { + let context: any; + + beforeEach(() => { + context = { + subscriptions: [], + }; + }); + + test("registers expected commands", () => { + activate(context); + expect(vscode.commands.registerCommand).toHaveBeenCalledWith( + "openproject.auth", + authorizeClient, + ); + expect(vscode.commands.registerCommand).toHaveBeenCalledWith( + "openproject.refresh", + refreshWPs, + ); + }); + + test("creates expected tree view", () => { + activate(context); + expect(vscode.window.createTreeView).toHaveBeenCalledWith( + "openproject-workspaces", + expect.any(Object), + ); + expect(OpenProjectTreeDataProvider.getInstance).toHaveBeenCalled(); + }); + + test("adds commands and subscriptions to context", () => { + activate(context); + expect(context.subscriptions).toHaveLength(2); + }); +}); + +describe("deactivate", () => { + it("nothing should happen", () => { + deactivate(); + }); +}); diff --git a/src/extension.ts b/src/extension.ts index 6c537b0..bc39f6f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,16 +1,22 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; +import OpenProjectTreeDataProvider from "./views/openProject.treeDataProvider"; +import authorizeClient from "./commands/authorizeClient.command"; +import refreshWPs from "./commands/refreshWPs.command"; export function activate(context: vscode.ExtensionContext) { - console.log('Congratulations, your extension "openproject" is now active!'); - - let disposable = vscode.commands.registerCommand('openproject.auth', () => { - vscode.window.showInputBox({ignoreFocusOut: true, password: true, prompt: "API token", title: "OpenProject API token"}).then(token => { - if (!token) {return;} - vscode.window.showInformationMessage(`Hello, ${token}!`); - vscode.commands.executeCommand("setContext", "openproject.token", token); - }); - }); - - context.subscriptions.push(disposable); + authorizeClient(); + const authCommand = vscode.commands.registerCommand( + "openproject.auth", + authorizeClient, + ); + const refreshWPsCommand = vscode.commands.registerCommand( + "openproject.refresh", + refreshWPs, + ); + vscode.window.createTreeView("openproject-workspaces", { + treeDataProvider: OpenProjectTreeDataProvider.getInstance(), + }); + context.subscriptions.push(authCommand, refreshWPsCommand); } + export function deactivate() {} From e128efae470a87cfc9c395b558777a9b9e2957f8 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:36:45 +0300 Subject: [PATCH 11/15] readme updated --- README.md | 57 ++++++------------------------------- pictures/work_packages.png | Bin 0 -> 41160 bytes 2 files changed, 8 insertions(+), 49 deletions(-) create mode 100644 pictures/work_packages.png diff --git a/README.md b/README.md index 08100db..a1ff845 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,23 @@ # openproject README -This is the README for your extension "openproject". After writing up a brief description, we recommend including the following sections. +Extension for [OpenProject](https://www.openproject.org/) - project management system. ## Features -Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - -For example if there is an image subfolder under your extension project workspace: - -\!\[feature X\]\(images/feature-x.png\) - -> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. +- Authorization. +- Getting list of your work packages. ![picture](https://github.com/bitswar/VSCodeOpenProject/pictures/work_packages.png) ## Requirements -If you have any requirements or dependencies, add a section describing those and how to install and configure them. +- Your OpenProject url +- Your OpenProject token ## Extension Settings -Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. - -For example: - This extension contributes the following settings: -* `myExtension.enable`: Enable/disable this extension. -* `myExtension.thing`: Set to `blah` to do something. - -## Known Issues - -Calling out known issues can help limit users opening duplicate issues against your extension. +* `openproject.base_url`: Your OpenProject url. +* `openproject.token`: Your OpenProject access token. ## Release Notes @@ -37,35 +25,6 @@ Users appreciate release notes as you update your extension. ### 1.0.0 -Initial release of ... - -### 1.0.1 - -Fixed issue #. - -### 1.1.0 - -Added features X, Y, and Z. - ---- - -## Following extension guidelines - -Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. - -* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) - -## Working with Markdown - -You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: - -* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). -* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). -* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. - -## For more information - -* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) -* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) +Initial release of OpenProject VSCode extension **Enjoy!** diff --git a/pictures/work_packages.png b/pictures/work_packages.png new file mode 100644 index 0000000000000000000000000000000000000000..15f72d7e99e277e646b8144ad9feb281f306b16a GIT binary patch literal 41160 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

7czm1xu$I|^*|dM; zdE2IB2Ku7d_RoO%`N zg3a4yU6$a!t5-=vyVTn;EF^8YMJeOXcymnPFtV>#a0PeDcq0Lg^i z6{wlgaTHzp{73pt)-gocY1Ip6&z6YKe;-SkH&eOyh3<8G#H1<4!27fXc?-lvv#PjSYU>zClAad5|thQ8fNGk;seWclyLh>V%x|KL_rHaZP%+JN-Kj+6t~3XWh(y$ z!pPRgfbf6&0&eJ~J5RlDvSStuRDXGNsDMStcZeSeQU6(zt8}uzbDZ=({s|1DhG~9i z%|;3o8U@_llmMgaE~upd`r}X2u|IyUD!T{+KbjVvJ&To{z_^{4yehqz9tg?*{-PLp z#E*+ro#HGEJ#VF8*UPWHl!{_%)e%FchELc{IWC?+I!^V8vXQ%-ctD_8SYdNU@4&)o zL=-cfe&Z|8!&s^2FB9cb9toJIO-!1-xWjgcS2QT+E2-|0VIG64UlmqHYVLhbCroJ9 z7Qz>%A0s&i7xXVh6|sE=>%(HUn%okV?s;pr)*;zJ=f(|3>ur<7SD#&MqZ7$BH=P13 z1n^sjt)Vlxp(?r}VskxCdg~Mw-VSs_xRncT8(Uwj?<_yIP%%i_psGm}k%yiw@T?>8 zp>E0^Ml2xE=!Z+E1-oDjfitb|&6jv0`5N*$t(U?@^m{!8J7UXj;pxVCq>B^%iy zDyy9pSFqNHQGQB9l8@_lq^HUaPsRsG&arl_EI#$MJTzM)7Zpr7MY8z4F?qhWPMPeu z21a<__IAAA&C2}HU5B%xWb+z77;u1Ff?zYQr(t^C+?jir6TUXnZP!F z7SGCOk0-7zogbDi(cFS1VTC=SE>bZfe$*Q3(@76$D=~*BD{Cmhyrlvlr^9VKNzF4h zpJw42)f4`tr`kWz(6B*X0{Vli0q!nWZ6c!d9AG?hijg5Rfo!Hrtsgf2JPSTP(?7b<$2=~k+ zradCW2?)487gUPCn7WMJ2}sQhahBwP3%^#E3zzl7^%atPpE}5UG>v|SE;QgV2)ahS zqt=LrZ?^^b$6AGvFl~yWZ_YUHj%REpAP?p6a@hr@T4!YLE=Hi%N1sTfy+-EYx!iY~ z+hg0UyIbv~78nu}Q5Mu1HhPHd@87ox?lHhd1>v=bKt&8Z0RErwfO&-yUhY;HoR$HW zNB19fbaAO0xRhEv#6nPFhT^Bx)OV4};Vl&MAutHRai>uXL~e}4RSE+~$PAv~F;Lw6 zM;$RT7mUhr391Fa=z*wy)xyfSA#F$k$#wX(AsW9k+PD@FB zIX46k8w+%WFs)sx5`+#=_A0wHAdp1ZSgYKU51WM-i|8?Y6+&9OpB=3G#4%g(r!pFY zAgOP7XD=+Fsa zgy0i!*)644oiEHW(%!Vr zDxoCzbai6s0#gNjXQz;n83V}Z5N^U1{?q-4g|WY;gdC@u(;~*Cp=2P%wN)ePq}qNt%zA!io+FGHL&g#Z8m literal 0 HcmV?d00001 From 2a28f31005b79a52ba9adfb260a2f653089354d8 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:38:18 +0300 Subject: [PATCH 12/15] repository added to package json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1bcfc98..3ef9ad4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "categories": [ "Other" ], + "repository": "https://github.com/bitswar/VSCodeOpenProject", "activationEvents": [], "main": "./dist/extension.js", "contributes": { From 39cd3b7f9a9566e89414092484eb72434b07573a Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 00:38:47 +0300 Subject: [PATCH 13/15] releases added to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index feaa83b..1ce764a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ package-lock.json dist out coverage +*.vsix From 0ba865833df5e61b70c57a56f0a232e0ba1dfe7e Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 13:48:58 +0300 Subject: [PATCH 14/15] LICENSE added --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From f83066ede5e1860a38524bc1ca6ed05ebadca299 Mon Sep 17 00:00:00 2001 From: goodhumored Date: Thu, 25 May 2023 13:49:48 +0300 Subject: [PATCH 15/15] publisher specified --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3ef9ad4..d349ced 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "openproject", + "publisher": "bitswar", "displayName": "OpenProject", "description": "OpenProject extension for VSCode", "version": "0.0.1",