├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── media ├── extension.js └── index.js ├── package-lock.json ├── package.json ├── resources ├── dark │ ├── more.svg │ └── refresh.svg ├── icons │ ├── tracker.png │ └── tracker.svg ├── light │ ├── more.svg │ └── refresh.svg └── screenshots │ └── sidebar.png ├── src ├── api.ts ├── credentials.ts ├── extension.ts ├── keychain.ts ├── resource.ts ├── test │ ├── extension.test.ts │ └── index.ts └── views │ ├── issuePanel.ts │ ├── issueWebView │ ├── App.tsx │ ├── index.css │ └── index.tsx │ └── issuesTree.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.vscode-typescript-tslint-plugin" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Launch Extension", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}", 11 | "--disable-extension=GitHub.vscode-pull-request-github-insiders" 12 | ], 13 | "skipFiles": ["/**/*.js", "**/node_modules/**/*.js"], 14 | "smartStep": true, 15 | "sourceMaps": true, 16 | "outFiles": [ 17 | "${workspaceFolder}/media/*.js" 18 | ] 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "${workspaceFolder}/out/test", 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "cSpell.enabled": false, 12 | "vscode-yandex-tracker.host": "https://st-api.yandex-team.ru" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-yandex-tracker" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-yandex-tracker 2 | 3 | VSCode and [Yandex.Tracker](https://yandex.ru/tracker/) integration 4 | 5 | ## Installation 6 | 7 | Install extension from [Marketplace](https://marketplace.visualstudio.com/items?itemName=rusnasonov.vscode-yandex-tracker) 8 | 9 | ## Authorization 10 | 11 | For Authorisation you need to get OAuth token and Organization ID. See [Api Access](https://tech.yandex.ru/connect/tracker/api/concepts/access-docpage/). 12 | 13 | 1. Run command `Yandex.Tracker: Setup OAuth token` in command pallet and pass token. 14 | 2. Open `Settings` and set `Organizatio ID`. 15 | 3. Reload window with `Developer: Reload Window` command. 16 | 17 | Token stored in system Keychain. 18 | 19 | ## Features 20 | 21 | ### Sidebar 22 | 23 | You can view you issues on sidebar. 24 | 25 | ![Sidebar](https://github.com/rusnasonov/vscode-yandex-tracker/blob/master/resources/screenshots/sidebar.png) 26 | 27 | You can explore issues with three filters — `Assign To Me`, `Followed By Me`, `Custom Query`. 28 | 29 | `Custom Query` filter can be changed in `Settings -> Query` field. See [Query Language](https://yandex.ru/tracker/support/user/query-filter.html). -------------------------------------------------------------------------------- /media/extension.js: -------------------------------------------------------------------------------- 1 | !function(e,t){for(var n in t)e[n]=t[n]}(exports,function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=16)}([function(e,t,n){"use strict";var r=n(5),o=n(25),s=Object.prototype.toString;function i(e){return"[object Array]"===s.call(e)}function a(e){return null!==e&&"object"==typeof e}function u(e){return"[object Function]"===s.call(e)}function c(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),i(e))for(var n=0,r=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},r.forEach(["delete","get","head"],function(e){u.headers[e]={}}),r.forEach(["post","put","patch"],function(e){u.headers[e]=r.merge(s)}),e.exports=u},function(e,t,n){"use strict";var r=n(7);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){e.exports=require("path")},function(e,t,n){"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r=300&&e.statusCode<400){if(this._currentRequest.removeAllListeners(),this._currentRequest.on("error",v),this._currentRequest.abort(),++this._redirectCount>this._options.maxRedirects)return void this.emit("error",new Error("Max redirects exceeded."));var n,o=this._options.headers;if(307!==e.statusCode&&!(this._options.method in f))for(n in this._options.method="GET",this._requestBodyBuffers=[],o)/^content-/i.test(n)&&delete o[n];if(!this._isRedirect)for(n in o)/^host$/i.test(n)&&delete o[n];var s=r.resolve(this._currentUrl,t);c("redirecting to",s),Object.assign(this._options,r.parse(s)),this._isRedirect=!0,this._performRequest(),e.destroy()}else e.responseUrl=this._currentUrl,e.redirects=this._redirects,this.emit("response",e),this._requestBodyBuffers=[]},e.exports=m({http:s,https:i}),e.exports.wrap=m},function(e,t){e.exports=require("url")},function(e,t,n){"use strict";e.exports=function(e){function t(e){for(var t=0,n=0;n{void 0!==e&&o.commands.executeCommand(`${t}.setOAuthToken`)});const v=new a.Tracker(f.default.create(),n.toString(),m||"",d),y=new u.IssuePanel(e,v),g=new i.IssuesProvider(v,l,"Resolution: empty() and Assignee: me()"),w=new i.IssuesProvider(v,l,"Resolution: empty() and Followers: me()"),C=new i.IssuesProvider(v,l,h);o.window.registerTreeDataProvider("assigned-to-me",g),o.window.registerTreeDataProvider("followed-by-me",w),o.window.registerTreeDataProvider("custom",C),o.commands.registerCommand(`${t}.refreshAssignToMeView`,()=>g.refresh()),o.commands.registerCommand(`${t}.refreshFollowedByMeView`,()=>w.refresh()),o.commands.registerCommand(`${t}.refreshCustomView`,()=>C.refresh()),o.commands.registerCommand(`${t}.moreAssignToMeView`,()=>g.loadMore()),o.commands.registerCommand(`${t}.moreFollowedByMeView`,()=>w.loadMore()),o.commands.registerCommand(`${t}.moreCustomView`,()=>C.loadMore()),o.commands.registerCommand(`${t}.setOAuthToken`,()=>r(this,void 0,void 0,function*(){const e=yield o.window.showInputBox({placeHolder:"Set OAuth token"});void 0!==e?(yield p.save(e),o.window.showInformationMessage(`Token saved for host ${n.authority}`,"Reload window").then(e=>{void 0!==e&&o.commands.executeCommand("workbench.action.reloadWindow")})):o.window.showErrorMessage("Token can't be empty")})),o.commands.registerCommand(`${t}.openIssue`,e=>r(this,void 0,void 0,function*(){void 0!==e||void 0!==(e=yield o.window.showInputBox({placeHolder:"Enter issue number"}))?(yield v.me(),y.show(e)):o.window.showErrorMessage("ID must't be empty")})),e.subscriptions.push(o.workspace.onDidChangeConfiguration(e=>{e.affectsConfiguration(`${t}`)&&o.window.showInformationMessage("Configuration changed","Reload window").then(e=>{void 0!==e&&o.commands.executeCommand("workbench.action.reloadWindow")})}))})},t.deactivate=function(){}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,s){function i(e){try{u(r.next(e))}catch(e){s(e)}}function a(e){try{u(r.throw(e))}catch(e){s(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,a)}u((r=r.apply(e,t||[])).next())})};Object.defineProperty(t,"__esModule",{value:!0});const o=n(18);class s extends Error{constructor(e){super(e),Object.setPrototypeOf(this,s.prototype)}}t.SecretNotFound=class extends s{};t.Credentials=class{constructor(e,t){this.extensionId=e,this.host=t}token(){return r(this,void 0,void 0,function*(){return yield o.keychain.getPassword(this.extensionId,this.host)})}save(e){return r(this,void 0,void 0,function*(){yield o.keychain.setPassword(this.extensionId,this.host,e)})}clean(){return r(this,void 0,void 0,function*(){yield o.keychain.deletePassword(this.extensionId,this.host)})}}},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const vscode=__webpack_require__(1);function getNodeModule(moduleName){const vscodeRequire=eval("require");try{return vscodeRequire(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`)}catch(e){}try{return vscodeRequire(`${vscode.env.appRoot}/node_modules/${moduleName}`)}catch(e){}}exports.keychain=getNodeModule("keytar")},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,s){function i(e){try{u(r.next(e))}catch(e){s(e)}}function a(e){try{u(r.throw(e))}catch(e){s(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,a)}u((r=r.apply(e,t||[])).next())})};Object.defineProperty(t,"__esModule",{value:!0});const o=n(1);t.IssuesProvider=class{constructor(e,t,n){this._onDidChangeTreeData=new o.EventEmitter,this.onDidChangeTreeData=this._onDidChangeTreeData.event,this.tracker=e,this.resource=t,this.query=n,this.issues=this.tracker.issues().search(n),this.nodes=[]}refresh(){this.issues=this.tracker.issues().search(this.query),this.nodes=[],this._onDidChangeTreeData.fire()}loadMore(){this._onDidChangeTreeData.fire()}getChildren(e){return r(this,void 0,void 0,function*(){let e=50;for(;0!==e;){const t=yield this.issues.next();if(void 0===t)break;this.nodes.push(t.value),e--}return this.nodes})}getTreeItem(e){return r(this,void 0,void 0,function*(){return{id:e.number(),label:e.number(),description:yield e.description(),tooltip:yield e.description(),iconPath:this.resource.icons.Tracker,command:{command:"vscode-yandex-tracker.openIssue",title:"Open Issue",arguments:[e.number()]}}})}};t.IssueItem=class{}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,s){function i(e){try{u(r.next(e))}catch(e){s(e)}}function a(e){try{u(r.throw(e))}catch(e){s(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,a)}u((r=r.apply(e,t||[])).next())})},o=this&&this.__await||function(e){return this instanceof o?(this.v=e,this):new o(e)},s=this&&this.__asyncValues||function(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e="function"==typeof __values?__values(e):e[Symbol.iterator](),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise(function(r,o){(function(e,t,n,r){Promise.resolve(r).then(function(t){e({value:t,done:n})},t)})(r,o,(t=e[n](t)).done,t.value)})}}},i=this&&this.__asyncDelegator||function(e){var t,n;return t={},r("next"),r("throw",function(e){throw e}),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,s){t[r]=e[r]?function(t){return(n=!n)?{value:o(e[r](t)),done:"return"===r}:s?s(t):t}:s}},a=this&&this.__asyncGenerator||function(e,t,n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var r,s=n.apply(e,t||[]),i=[];return r={},a("next"),a("throw"),a("return"),r[Symbol.asyncIterator]=function(){return this},r;function a(e){s[e]&&(r[e]=function(t){return new Promise(function(n,r){i.push([e,t,n,r])>1||u(e,t)})})}function u(e,t){try{(n=s[e](t)).value instanceof o?Promise.resolve(n.value.v).then(c,f):d(i[0][2],n)}catch(e){d(i[0][3],e)}var n}function c(e){u("next",e)}function f(e){u("throw",e)}function d(e,t){e(t),i.shift(),i.length&&u(i[0][0],i[0][1])}};Object.defineProperty(t,"__esModule",{value:!0});t.Tracker=class{constructor(e,t,n,r=""){this._client=e,this.host=t,this.token=n,this.orgId=r,this.frontByHost=new Map([["https://api.tracker.yandex.net/","https://tracker.yandex.ru"],["https://st-api.test.yandex-team.ru/","https://st.test.yandex-team.ru"],["https://st-api.yandex-team.ru/","https://st.yandex-team.ru"]])}client(){return this._client.defaults.baseURL=`${this.host}v2`,this._client.defaults.headers.common.Authorization=`OAuth ${this.token}`,this._client.defaults.headers.common["X-Org-Id"]=this.orgId,this._client}issues(){return new c(this.client())}front(){if(!this.frontByHost.has(this.host))throw Error(`Front for host ${this.host} not found`);return this.frontByHost.get(this.host)||""}me(){return r(this,void 0,void 0,function*(){let e=(yield this.client().get("/myself")).data;return new u(this.client(),e.uid)})}};class u{constructor(e,t){this.client=e,this.uid=t}raw(){return r(this,void 0,void 0,function*(){return(yield this.client.get(`/users/${this.uid}`)).data})}}t.User=u;class c{constructor(e){this.client=e}search(e){return a(this,arguments,function*(){let t=1;for(;;){const n=yield o(this.client.post("/issues/_search",{query:e},{params:{page:t,perPage:50}}));if(yield o(yield*i(s(n.data.map(e=>new f(this.client,e.key,e.summary))))),!n.headers.link.includes('rel="next"'))break;t++}})}get(e){return new f(this.client,e)}}t.Issues=c;class f{constructor(e,t,n=""){this.client=e,this.num=t,this.dsc=n}number(){return this.num}description(){return r(this,void 0,void 0,function*(){return""===this.dsc?(yield this.raw()).summary:this.dsc})}raw(){return r(this,void 0,void 0,function*(){return(yield this.client.get(`/issues/${this.num}`)).data})}comments(){return new d(this.client,this.num)}}t.Issue=f;class d{constructor(e,t){this.client=e,this.issueNumber=t}all(){return r(this,void 0,void 0,function*(){return(yield this.client.get(`/issues/${this.issueNumber}/comments`)).data.map(e=>new h(this.client,this.issueNumber,e.id,e))})}}t.Comments=d;class h{constructor(e,t,n,r){this.client=e,this.issueNumber=t,this.commentId=n,this.rawComment=r}raw(){return r(this,void 0,void 0,function*(){if(void 0!==this.rawComment)return Promise.resolve(this.rawComment);return(yield this.client.get(`/issues/${this.issueNumber}/comments/${this.commentId}`)).data})}}t.Comment=h},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,s){function i(e){try{u(r.next(e))}catch(e){s(e)}}function a(e){try{u(r.throw(e))}catch(e){s(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,a)}u((r=r.apply(e,t||[])).next())})};Object.defineProperty(t,"__esModule",{value:!0});const o=n(4),s=n(1);t.IssuePanel=class{constructor(e,t){this.context=e,this.tracker=t,this.disposables=[],this.panel=null}show(e){return r(this,void 0,void 0,function*(){const t=(s.window.activeTextEditor?s.window.activeTextEditor.viewColumn:void 0)||s.ViewColumn.One;this.panel?(this.panel.reveal(t),this.panel.title=e):(this.panel=s.window.createWebviewPanel("react",e,t,{enableScripts:!0,localResourceRoots:[s.Uri.file(o.join(this.context.extensionPath,"media"))]}),this.panel.webview.html=this.webviewHTML(),this.panel.onDidDispose(()=>this.dispose(),null,this.disposables));const n=this.tracker.issues().get(e),r=yield Promise.all((yield n.comments().all()).map(e=>e.raw()));this.panel.webview.postMessage({command:"issue",args:{issue:yield n.raw(),front:this.tracker.front(),comments:r}})})}dispose(){if(null!==this.panel){for(this.panel.dispose();this.disposables.length;){const e=this.disposables.pop();e&&e.dispose()}this.panel=null}}webviewHTML(){const e=s.Uri.file(o.join(this.context.extensionPath,"media","index.js")).with({scheme:"vscode-resource"}),t=this.nonce();return`\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n Issues\n \n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t
\n 79 | 80 | `; 81 | } 82 | 83 | private nonce() { 84 | let text = ''; 85 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 86 | for (let i = 0; i < 32; i++) { 87 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 88 | } 89 | return text; 90 | } 91 | } -------------------------------------------------------------------------------- /src/views/issueWebView/App.tsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { RawIssue, RawComment } from '../../api'; 3 | 4 | export interface State { 5 | issue: RawIssue | null; 6 | front: string; 7 | comments: RawComment[]; 8 | } 9 | 10 | interface Message { 11 | command: string; 12 | args: any; 13 | } 14 | 15 | class App extends Component { 16 | constructor() { 17 | super(); 18 | this.reciveExtensionMessage = this.reciveExtensionMessage.bind(this); 19 | this.state = { 20 | issue: null, 21 | front: '', 22 | comments: [] 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | window.addEventListener('message', this.reciveExtensionMessage); 28 | } 29 | 30 | reciveExtensionMessage(event: any) { 31 | const message: Message = event.data; 32 | 33 | switch (message.command) { 34 | case 'issue': { 35 | this.setState({ 36 | issue: message.args.issue, 37 | front: message.args.front, 38 | comments: message.args.comments 39 | }); 40 | break; 41 | } 42 | default: 43 | break; 44 | } 45 | 46 | } 47 | 48 | public render(props: any, state: State) { 49 | if(!state.issue) { 50 | return null; 51 | } 52 | const issue = state.issue; 53 | const created = new Date(issue.createdAt); 54 | const updated = new Date(issue.updatedAt); 55 | 56 | return ( 57 |
58 |
59 |

{issue.summary}

60 |
61 | 62 |
{issue.status.display}
63 |
Created: {created.toDateString()}
64 |
Updated: {updated.toDateString()}
65 |
66 |
67 |
68 | {state.comments.map((cmt) => { 69 | return ( 70 |
71 |
{cmt.createdBy ? cmt.createdBy.display : ''}
72 |
73 |
74 | ); 75 | })} 76 |
77 |
78 | 110 |
111 | ); 112 | } 113 | } 114 | 115 | export default App; -------------------------------------------------------------------------------- /src/views/issueWebView/index.css: -------------------------------------------------------------------------------- 1 | .Issue { 2 | display: flex; 3 | } 4 | 5 | .Issue > .Main { 6 | width: 70%; 7 | } 8 | 9 | .Issue > .Sidebar { 10 | margin-left: 40px; 11 | } 12 | 13 | .Main > .Meta { 14 | display: flex; 15 | margin-top: 10px; 16 | margin-bottom: 10px; 17 | padding: 10px; 18 | border-bottom: 1px solid white; 19 | } 20 | 21 | .Main > .Description { 22 | margin-top: 10px; 23 | border-bottom: 1px solid white; 24 | padding-top: 40px; 25 | padding-bottom: 40px; 26 | } 27 | 28 | .Description div { 29 | color: var(--vscode-editor-foreground) !important; 30 | } 31 | 32 | .Meta > .Item { 33 | margin-left: 5px; 34 | margin-right: 5px; 35 | } 36 | 37 | .Comments > .Comment { 38 | margin-top: 10px; 39 | margin-bottom: 10px; 40 | padding: 10px; 41 | border-bottom: 1px solid white; 42 | } 43 | 44 | .Author { 45 | font-weight: bold; 46 | margin-bottom: 5px; 47 | } 48 | 49 | .Author::first-letter { 50 | color: red; 51 | } 52 | 53 | .Sidebar td { 54 | padding-bottom: 10px; 55 | } -------------------------------------------------------------------------------- /src/views/issueWebView/index.tsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import App from './App'; 3 | 4 | import './index.css'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') as HTMLElement 9 | ); -------------------------------------------------------------------------------- /src/views/issuesTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Tracker, Issue } from '../api'; 3 | import { Resource } from '../resource'; 4 | 5 | export class IssuesProvider implements vscode.TreeDataProvider { 6 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 7 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 8 | 9 | private tracker: Tracker; 10 | 11 | private resource: Resource; 12 | 13 | private query: string; 14 | 15 | private issues: AsyncIterator; 16 | 17 | private nodes: Issue[]; 18 | 19 | constructor(tracker: Tracker, resource: Resource, query: string) { 20 | this.tracker = tracker; 21 | this.resource = resource; 22 | this.query = query; 23 | this.issues = this.tracker.issues().search(query); 24 | this.nodes = []; 25 | } 26 | 27 | refresh() { 28 | this.issues = this.tracker.issues().search(this.query); 29 | this.nodes = []; 30 | this._onDidChangeTreeData.fire(); 31 | } 32 | 33 | loadMore() { 34 | this._onDidChangeTreeData.fire(); 35 | } 36 | 37 | async getChildren(element?: IssueItem): Promise { 38 | let perPanel = 50; 39 | while(perPanel !== 0){ 40 | const issue = await this.issues.next(); 41 | if (issue === undefined) { 42 | break; 43 | } 44 | this.nodes.push(issue.value); 45 | perPanel--; 46 | } 47 | return this.nodes; 48 | } 49 | 50 | async getTreeItem(element: Issue): Promise { 51 | return { 52 | id: element.number(), 53 | label: element.number(), 54 | description: await element.description(), 55 | tooltip: await element.description(), 56 | iconPath: this.resource.icons.Tracker, 57 | command: { 58 | command: 'vscode-yandex-tracker.openIssue', 59 | title: 'Open Issue', 60 | arguments: [element.number()], 61 | } 62 | }; 63 | } 64 | } 65 | 66 | export class IssueItem implements vscode.TreeItem { 67 | 68 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "noUnusedLocals": true, 7 | "lib": [ 8 | "dom", 9 | "esnext.asynciterable", 10 | "es6" 11 | ], 12 | "sourceMap": true, 13 | "rootDir": "./src", 14 | "jsx": "react", 15 | "jsxFactory": "h", 16 | "typeRoots": [ 17 | "./node_modules/@types", 18 | "./src/typings/" 19 | ], 20 | "noImplicitAny": true, 21 | "strictNullChecks": true 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | ".vscode-test" 29 | ] 30 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import-spacing": true, 4 | "indent": [true, "spaces"], 5 | "newline-before-return": false, 6 | "no-consecutive-blank-lines": true, 7 | "no-empty-line-after-opening-brace": false, 8 | "no-irregular-whitespace": true, 9 | "object-literal-key-quotes": [true, "as-needed"], 10 | "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], 11 | "quotemark": [true, "single"], 12 | "space-within-parens": true, 13 | "typedef-whitespace": false, 14 | "no-unused-expression": true, 15 | "no-duplicate-variable": true, 16 | "no-shadowed-variable": true, 17 | "no-string-throw": true, 18 | "no-bitwise": true, 19 | "no-var-keyword": true, 20 | "no-trailing-whitespace": true, 21 | "curly": true, 22 | "class-name": true, 23 | "semicolon": [ 24 | true, 25 | "always" 26 | ], 27 | "triple-equals": true, 28 | "whitespace": true 29 | } 30 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TSLintPlugin = require('tslint-webpack-plugin'); 4 | 5 | function getWebviewConfig(env) { 6 | /** @type webpack.Configuration */ 7 | let webview = { 8 | name: 'webiew', 9 | mode: env.production ? 'production' : 'development', 10 | entry: { 11 | index: './src/views/issueWebView/index.tsx' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/ 19 | }, 20 | { 21 | test: /\.css/, 22 | use: ['style-loader', 'css-loader'] 23 | }, 24 | { 25 | test: /\.svg/, 26 | use: ['svg-inline-loader'] 27 | } 28 | ] 29 | }, 30 | resolve: { 31 | extensions: ['.tsx', '.ts', '.js', '.svg'] 32 | }, 33 | devtool: !env.production ? 'inline-source-map' : undefined, 34 | output: { 35 | filename: '[name].js', 36 | path: path.resolve(__dirname, 'media') 37 | }, 38 | plugins: [ 39 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 40 | new TSLintPlugin({ 41 | files: ['./src/**/*.ts'] 42 | }) 43 | ] 44 | }; 45 | 46 | return webview; 47 | } 48 | 49 | /** 50 | * 51 | * @param {*} env 52 | * @returns webpack.Configuration 53 | */ 54 | function getExtensionConfig(env) { 55 | /** @type webpack.Configuration */ 56 | let config = { 57 | name: 'extension', 58 | mode: env.production ? 'production' : 'development', 59 | target: 'node', 60 | entry: { 61 | extension: './src/extension.ts' 62 | }, 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.tsx?$/, 67 | use: 'ts-loader', 68 | exclude: /node_modules/ 69 | }, 70 | // FIXME: apollo-client uses .mjs, which imposes hard restrictions 71 | // on imports available from other callers. They probably didn't know 72 | // this. They just used .mjs because it seemed new and hip. 73 | // 74 | // We should either fix or remove that package, then remove this rule, 75 | // which introduces nonstandard behavior for mjs files, which are 76 | // terrible. This is all terrible. Everything is terrible.👇🏾 77 | { 78 | test: /\.mjs$/, 79 | include: /node_modules/, 80 | type: "javascript/auto", 81 | }, 82 | { 83 | test: /\.gql/, 84 | loader: 'graphql-tag/loader', 85 | exclude: /node_modules/ 86 | } 87 | ] 88 | }, 89 | resolve: { 90 | extensions: ['.tsx', '.ts', '.js'], 91 | alias: { 92 | "node-fetch": path.resolve(__dirname, 'node_modules/node-fetch/lib/index.js'), 93 | } 94 | }, 95 | devtool: !env.production ? 'source-map' : undefined, 96 | output: { 97 | filename: '[name].js', 98 | path: path.resolve(__dirname, 'media'), 99 | libraryTarget: "commonjs", 100 | devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]' 101 | }, 102 | externals: { 103 | 'vscode': 'commonjs vscode', 104 | 'utf-8-validate': 'utf-8-validate', 105 | 'bufferutil': 'bufferutil', 106 | 'encoding': 'encoding' 107 | } 108 | }; 109 | 110 | return config; 111 | } 112 | 113 | module.exports = function(env) { 114 | env = env || {}; 115 | env.production = !!env.production; 116 | return [getExtensionConfig(env), getWebviewConfig(env)]; 117 | }; --------------------------------------------------------------------------------