├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── LICENSE-MIT ├── README.md ├── gulpfile.js ├── package.json └── src ├── _locales └── en │ └── messages.json ├── css └── improved.css ├── images ├── github-16.png ├── github-32.png ├── icon-128.png ├── icon-16.png ├── icon-19.png └── icon-38.png ├── js ├── background.js ├── content-script.js ├── content │ ├── customfields.js │ ├── index.js │ ├── page.js │ ├── tickets │ │ ├── epic-tickets.js │ │ └── issue-tickets.js │ ├── ui │ │ ├── avatar.js │ │ ├── emptyColumn.js │ │ └── filter.js │ └── util │ │ ├── api.js │ │ ├── caching.js │ │ ├── favicon.js │ │ ├── findPRs.js │ │ ├── fromQueryString.js │ │ ├── randomRGB.js │ │ └── sizzle-customizations.js ├── live-reload.js ├── options.js └── popup.js ├── manifest.json ├── manifest.tmpl.json └── views ├── options.html └── popup.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zips/ 2 | /dist/ 3 | /node_modules/ 4 | /src/js/dist/ 5 | /src/js/manifest.json 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals" : { 22 | "chrome": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Dylan Greene 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jira Improved 2 | 3 | #### Improves the Agile and Kanban boards! 4 | 5 | * Text filtering on every board *Hotkey: `f`* 6 | * Hide the Done column if you use a quick filter to hide those tickets. 7 | * Shorter page headers to give more room to the boards. 8 | 9 | ### Epic Board Improved 10 | 11 | ![screen shot 2014-08-18 at 10 43 23 am](https://cloud.githubusercontent.com/assets/51505/3952810/09ab6868-26e6-11e4-937e-65a1abe4db53.png) 12 | 13 | * Issue count for Blue: `Backlog`, Yellow: `In Progress`, Green: `Closed`. 14 | * Hover over Epic to see every issue assigned to that Epic. 15 | * Click on an issue to jump to that issue. 16 | 17 | ### Issue Board Improved 18 | 19 | ![screen shot 2014-08-18 at 10 43 01 am](https://cloud.githubusercontent.com/assets/51505/3952812/0b41ad40-26e6-11e4-98b4-6448ac0cb5b7.png) 20 | 21 | * See what Epic issues are part of. 22 | * Red: `Backlog`. Blue: `In Progress`. Green: `Done`. 23 | * Show a Github icon for every PR associated with the ticket. 24 | * Closed PR's are colored green. 25 | 26 | ### Add to Chrome 27 | 28 | [Add to Chrome](https://chrome.google.com/webstore/detail/jira-improved/mdfbpeoaadkecmpingophakekbicinip) 29 | 30 | ### Building locally 31 | 32 | ```bash 33 | # Install dependencies 34 | $ npm install 35 | 36 | # Auto-rebuild and reload extension as you make changes 37 | $ gulp 38 | 39 | # Increase the version and build a zip file. 40 | $ gulp build 41 | ``` 42 | 43 | 44 | ### Disclaimers 45 | 46 | Not created owned or supported by Atlassian/Jira. 47 | 48 | #### Should be safe to use 49 | 50 | * This plugin will not make changes to Jira. 51 | * This plugin does not requrie any special permissions. 52 | * This plugin will never ask for your username or password. 53 | * It is safe to try on any Jira install. 54 | 55 | Because everybody uses Jira differently I can't promise that it will work as well for you as it does for me, especially 56 | because I utilize some custom fields that I'm not sure are the same in every Jira install. 57 | Tell me what field you use and I'll see if I can auto-detect it or I'll create an options UI. 58 | 59 | #### Custom fields 60 | 61 | ```js 62 | var CUSTOM_FIELD_EPIC_NAME = 'customfield_13259'; 63 | var CUSTOM_FIELD_EPIC_PARENT = 'customfield_13258'; 64 | var CUSTOM_FIELD_PULL_REQUESTS = 'customfield_13153'; 65 | ``` 66 | 67 | ### Contributing 68 | 69 | I accept Pull Requests. 70 | 71 | 72 | ### License 73 | 74 | Copyright (c) 2014 Dylan Greene. 75 | 76 | Released under the [MIT license](https://tldrlegal.com/license/mit-license). 77 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var gutil = require('gulp-util'); 5 | //var debug = require('gulp-debug'); 6 | 7 | var source = require('vinyl-source-stream'); 8 | var watchify = require('watchify'); 9 | var browserify = require('browserify'); 10 | var jshint = require('gulp-jshint'); 11 | var stylish = require('jshint-stylish'); 12 | 13 | var zip = require('gulp-zip'); 14 | var bump = require('gulp-bump'); 15 | var jeditor = require('gulp-json-editor'); 16 | var rename = require('gulp-rename'); 17 | 18 | var watch = require('gulp-watch'); 19 | var livereload = require('gulp-livereload'); 20 | 21 | var chalk = require('chalk'); 22 | 23 | var babelify = require('babelify'); 24 | 25 | var uglify = require('gulp-uglify'); 26 | var buffer = require('vinyl-buffer'); 27 | 28 | 29 | gulp.task('browserify', function() { 30 | var bundler = watchify(browserify({ 31 | entries: ['./src/js/content/index'], 32 | debug: true 33 | }, 34 | watchify.args 35 | ).transform( 36 | babelify, 37 | { 38 | presets: ['env'], 39 | plugins: ['transform-runtime'] 40 | } 41 | )); 42 | 43 | // Optionally, you can apply transforms 44 | // and other configuration options on the 45 | // bundler just as you would with browserify 46 | //bundler.transform('brfs'); 47 | 48 | function rebundle() { 49 | return bundler.bundle() 50 | .on('error', function(e) { 51 | gutil.log(chalk.red('Error: ' + e.message)); 52 | }) 53 | .pipe(source('bundle.js')) 54 | .pipe(gulp.dest('src/js/dist')) 55 | .pipe(livereload()); 56 | } 57 | 58 | bundler 59 | .on('update', rebundle); 60 | // log errors if they happen; 61 | 62 | return rebundle(); 63 | }); 64 | 65 | gulp.task('browserify-minified', ['manifest'], function() { 66 | var bundler = browserify({ 67 | entries: ['./src/js/content/index'] 68 | } 69 | ).transform( 70 | babelify, 71 | { 72 | presets: ['env'], 73 | plugins: ['transform-runtime'] 74 | } 75 | ); 76 | 77 | // Optionally, you can apply transforms 78 | // and other configuration options on the 79 | // bundler just as you would with browserify 80 | //bundler.transform('brfs'); 81 | 82 | function rebundle() { 83 | return bundler.bundle() 84 | .on('error', function(e) { 85 | gutil.log(chalk.red('Error: ' + e.message)); 86 | }) 87 | .pipe(source('bundle.js')) 88 | .pipe(buffer()) 89 | .pipe(uglify()) 90 | .pipe(gulp.dest('src/js/dist')); 91 | } 92 | 93 | bundler 94 | .on('update', rebundle); 95 | // log errors if they happen; 96 | 97 | return rebundle(); 98 | }); 99 | 100 | 101 | gulp.task('jshint-watch', function(){ 102 | return watch([ 103 | 'gulpfile.js', 104 | 'src/**/*.js', 105 | '!src/js/dist/**' 106 | ]) 107 | .pipe(jshint()) 108 | .pipe(jshint.reporter(stylish)); 109 | }); 110 | 111 | gulp.task('jshint', function(){ 112 | return gulp.src(['gulpfile.js', 113 | 'src/**/*.js', 114 | '!src/js/dist/**' 115 | ]) 116 | .pipe(jshint()) 117 | .pipe(jshint.reporter(stylish)); 118 | }); 119 | 120 | gulp.task('zip', ['browserify-minified'], function () { 121 | 122 | var pkg = require('./package.json'); 123 | 124 | 125 | return gulp.src(['./src/**']) 126 | .pipe(zip(pkg.name + '-' + pkg.version + '.zip', {compress: true})) 127 | .pipe(gulp.dest('zips')); 128 | }); 129 | 130 | gulp.task('bump', function(){ 131 | return gulp.src('./package.json') 132 | .pipe(bump()) 133 | .pipe(gulp.dest('./')); 134 | }); 135 | 136 | gulp.task('manifest', ['bump'], function(){ 137 | return gulp.src('./src/manifest.tmpl.json') 138 | .pipe(jeditor(function(json){ 139 | json.version = require('./package.json').version; 140 | return json; 141 | })) 142 | .pipe(rename('manifest.json')) 143 | .pipe(gulp.dest('./src')); 144 | }); 145 | 146 | gulp.task('manifest-livereload', function(){ 147 | return gulp.src('./src/manifest.tmpl.json') 148 | .pipe(jeditor(function(json){ 149 | json.version = Math.floor(Math.random() * 1000) + '' + require('./package.json').version; 150 | json.background.scripts.push('js/live-reload.js'); 151 | return json; 152 | })) 153 | .pipe(rename('manifest.json')) 154 | .pipe(gulp.dest('./src')); 155 | }); 156 | 157 | gulp.task('live-reload', livereload.listen); 158 | 159 | gulp.task('watch',function() { 160 | watch(['src/**', '!src/js/content/**'], function(files, cb) { 161 | files.pipe(livereload({auto: false}), cb); 162 | }); 163 | }); 164 | 165 | gulp.task('test', ['jshint']); 166 | gulp.task('default', ['live-reload', 'manifest-livereload', 'watch', 'jshint-watch', 'browserify']); 167 | gulp.task('build', ['zip']); 168 | 169 | // todo: https://github.com/erikdesjardins/chrome-extension-deploy 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-improved", 3 | "description": "Chrome plugin for improving Jira Kanban and Agile views.", 4 | "version": "2.0.9", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "gulp test", 8 | "build": "gulp build" 9 | }, 10 | "private": false, 11 | "keywords": [ 12 | "jira", 13 | "kanban", 14 | "agile", 15 | "chrome extension", 16 | "chrome plugin" 17 | ], 18 | "author": { 19 | "name": "Dylan Greene", 20 | "email": "dylang@gmail.com" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/dylang/jira-improved/issues" 24 | }, 25 | "homepage": "https://github.com/dylang/jira-improved", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/dylang/jira-improved.git" 29 | }, 30 | "dependencies": { 31 | "babel-runtime": "^6.26.0", 32 | "babelify": "^7.3.0", 33 | "chrome-extension-deploy": "^3.0.0", 34 | "co": "^4.6.0", 35 | "got": "^7.1.0", 36 | "lodash": "^3.7.0", 37 | "lscache": "^1.1.0", 38 | "seed-random": "^2.2.0" 39 | }, 40 | "devDependencies": { 41 | "babel-plugin-transform-runtime": "^6.23.0", 42 | "babel-preset-env": "^1.6.0", 43 | "browserify": "^14.4.0", 44 | "browserify-shim": "^3.8.14", 45 | "chalk": "^2.1.0", 46 | "escape-html": "^1.0.3", 47 | "get-urls": "^7.0.0", 48 | "gulp": "^3.9.1", 49 | "gulp-bump": "^2.8.0", 50 | "gulp-debug": "^3.1.0", 51 | "gulp-jshint": "^2.0.4", 52 | "gulp-json-editor": "^2.2.1", 53 | "gulp-livereload": "^3.8.1", 54 | "gulp-rename": "^1.2.2", 55 | "gulp-uglify": "^3.0.0", 56 | "gulp-util": "^3.0.8", 57 | "gulp-watch": "^4.3.11", 58 | "gulp-zip": "^4.0.0", 59 | "jshint": "^2.9.5", 60 | "jshint-stylish": "^2.2.1", 61 | "vinyl-buffer": "^1.0.0", 62 | "vinyl-source-stream": "^1.1.0", 63 | "watchify": "^3.9.0" 64 | }, 65 | "engines": { 66 | "node": ">=4" 67 | }, 68 | "license": "MIT" 69 | } 70 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Jira Improved", 4 | "description": "Improved Jira Agile boards!" 5 | }, 6 | "appDescription": { 7 | "message": "Improved Jira Agile boards!", 8 | "description": "Improved Jira Agile boards!" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/css/improved.css: -------------------------------------------------------------------------------- 1 | /* 2 | class="aui-label" 3 | */ 4 | 5 | .ghx-issue { 6 | height: auto !important;; 7 | min-height: 31px; 8 | margin-top: 3px !important; 9 | margin-bottom: 2px !important; 10 | margin-left: 2px !important; 11 | margin-right: 1px !important; 12 | font-size: 12px !important; 13 | padding: 3px 0px 2px 25px !important; 14 | display: block; 15 | line-height: 1.1em; 16 | } 17 | 18 | 19 | .ghx-issue-fields .ghx-key { 20 | margin-top: 3px !important; 21 | margin-bottom: 2px !important; 22 | } 23 | 24 | .ghx-issue .ghx-grabber, 25 | .ghx-issue:first-child .ghx-grabber { 26 | height: 100% !important; 27 | width: 2px; 28 | } 29 | 30 | .chrome .ghx-issue-fields .ghx-summary .ghx-inner, 31 | .ghx-issue-fields .ghx-summary .ghx-inner { 32 | max-height: none !important; 33 | height: auto !important; 34 | margin-bottom: 4px; 35 | display: inherit; 36 | overflow: inherit !important; 37 | } 38 | 39 | 40 | .ghx-issue .fixVersion-container .fixVersion, 41 | .ghx-issue .label-container .label, 42 | .ghx-issue .epic-link { 43 | overflow: hidden; 44 | max-width: 100%; 45 | transition: opacity 0.25s ease-in-out; 46 | ppopacity: 0.7; 47 | 48 | display: inline-block; 49 | padding: 2px 4px; 50 | font-size: 9px; 51 | line-height: 10px; 52 | color: #ffffff; 53 | white-space: nowrap; 54 | vertical-align: baseline; 55 | text-overflow: ellipsis; 56 | } 57 | 58 | .ghx-issue:hover .epic-link, 59 | .ghx-issue .epic-link:hover { 60 | opacity: 1; 61 | } 62 | 63 | .ghx-issue .epic-link .status { 64 | } 65 | 66 | .ghx-issue .fixVersion-container .fixVersion, 67 | .ghx-issue .label-container .label { 68 | background: #f5f5f5; 69 | border: 1px solid #ccc; 70 | color: #383838; 71 | border-radius: 3px; 72 | margin-right: 3px; 73 | } 74 | 75 | 76 | .ghx-issue .ghx-type { 77 | height: auto !important; 78 | overflow: auto !important; 79 | } 80 | 81 | .ghx-issue .pull-request, 82 | .ghx-issue .pull-request-other { 83 | width: 16px; 84 | height: 16px; 85 | display: inline-block; 86 | vertical-align: middle; 87 | box-shadow: none; 88 | transition: box-shadow 0.25s ease-in-out; 89 | } 90 | 91 | .ghx-issue .pull-request-other img { 92 | width: 16px; 93 | height: 16px; 94 | } 95 | 96 | .ghx-issue .pull-request-other .ghx-avatar-img { 97 | width: 16px; 98 | height: 16px; 99 | line-height: 16px; 100 | font-size: 11px; 101 | display: none; 102 | } 103 | 104 | .ghx-issue .pull-request-other.broken-image img { 105 | display: none; 106 | } 107 | 108 | .ghx-issue .pull-request-other.broken-image span { 109 | display: inline-block; 110 | } 111 | 112 | .ghx-issue .pull-request { 113 | background-image: -webkit-image-set( 114 | url(chrome-extension://__MSG_@@extension_id__/images/github-16.png) 1x, 115 | url(chrome-extension://__MSG_@@extension_id__/images/github-32.png) 2x 116 | ); 117 | } 118 | 119 | .ghx-issue .pull-request-other:hover, 120 | .ghx-issue .pull-request:hover { 121 | box-shadow: 0 0 5px 2px #9ecaed; 122 | } 123 | 124 | .ghx-issue .pull-request-unknown { 125 | opacity: 0.5; 126 | } 127 | 128 | .ghx-issue .pull-request-error { 129 | -webkit-filter: invert(1); 130 | } 131 | 132 | .ghx-issue .pull-request-open { 133 | 134 | } 135 | 136 | .ghx-issue .pull-request-closed { 137 | -webkit-filter: hue-rotate(90deg); 138 | } 139 | 140 | 141 | .ghx-issue .ji-font-smaller { 142 | font-size: 9px !important; 143 | } 144 | 145 | .ghx-issue .kinda-hidden { 146 | display: none; 147 | } 148 | 149 | .ghx-parent-group .highlight, 150 | .ghx-issue .highlight { 151 | background-color: rgba(255,196,13, 0.2); 152 | box-shadow: 153 | 0 2px 2px rgba(255,196,13, 0.2), 154 | 0 0 4px 1px rgba(255,196,13, 0.2) !important; 155 | 156 | } 157 | 158 | .ghx-issue .expanding-tag { 159 | border-radius: 3px; 160 | } 161 | 162 | .ghx-issue .expanding-tag:hover { 163 | z-index: 10000; 164 | position: relative; 165 | max-width: none; 166 | text-decoration: none; 167 | 168 | box-shadow: 169 | 0 2px 2px rgba(0,0,0,0.3), 170 | 0 0 4px 1px rgba(0,0,0,0.2); 171 | } 172 | 173 | .ghx-issue .expanding-tag:hover .kinda-hidden { 174 | ddisplay: inherit; 175 | } 176 | 177 | 178 | .improved .jira-issue-status-lozenge, 179 | .ghx-issue .aui-lozenge { 180 | font-weight: normal; 181 | text-align: left; 182 | text-transform: none; 183 | width: auto; 184 | max-width: 100%; 185 | } 186 | 187 | .ghx-swimlane-header .improved .aui-lozenge { 188 | display: inline-block; 189 | } 190 | 191 | 192 | .ghx-issue .traffic-light { 193 | margin-left: -20px; 194 | } 195 | 196 | .ghx-issue .issues, 197 | .ghx-swimlane-header .issues { 198 | display: none; 199 | margin-left: -20px; 200 | margin-right: 15px; 201 | line-height: 1.5em; 202 | background: red; 203 | position: absolute; 204 | z-index: 10000000; 205 | background: rgba(225,225,225,0.9); 206 | border-radius: 4px; 207 | padding: 5px; 208 | box-shadow: 209 | 0 2px 2px rgba(0,0,0,0.3), 210 | 0 0 4px 1px rgba(0,0,0,0.2); 211 | } 212 | 213 | .ghx-issue:hover .issues, 214 | .ghx-swimlane-header:hover .issues { 215 | display: block; 216 | } 217 | 218 | .ghx-issue .epic-container, 219 | .ghx-issue .pull-requests, 220 | .ghx-issue .label-container, 221 | .ghx-issue .fixVersion-container { 222 | margin-left: -20px; 223 | margin-right: 15px; 224 | } 225 | 226 | 227 | .ghx-issue .epic-container .summary { 228 | color: #777 !important; 229 | } 230 | 231 | .ghx-issue .epic-container .summary-text { 232 | color: #fff !important; 233 | } 234 | 235 | .ghx-issue .epic-container .closed { 236 | text-decoration: line-through; 237 | } 238 | 239 | .ghx-issue .epic-container a:hover .closed { 240 | text-decoration: none; 241 | } 242 | 243 | 244 | .ghx-band-1 .ghx-issue .ghx-issue-content, 245 | .ghx-band-2 .ghx-issue .ghx-issue-content, 246 | .ghx-issue .ghx-issue-content { 247 | padding: 2px; 248 | min-height: 0; 249 | } 250 | 251 | .ghx-days { 252 | position: relative; 253 | width: inherit; 254 | margin-left: -35px; 255 | } 256 | 257 | .ghx-agile .aui-label { 258 | font-size: 10px !important; 259 | } 260 | 261 | /** 262 | Try to make it so the whole page will scroll 263 | 264 | .ghx-column-header-group { 265 | position: initial !important; 266 | } 267 | 268 | .ghx-work { 269 | height: auto !important; 270 | } 271 | 272 | 273 | .ghx-scroll-columns, #ghx-plan, #ghx-report, #ghx-work { 274 | overflow: visible !important; 275 | height: auto !important; 276 | } 277 | 278 | **/ 279 | 280 | #ghx-header { 281 | padding: 2px 20px !important; 282 | } 283 | 284 | #ghx-column-headers { 285 | padding-top: 5px !important; 286 | } 287 | 288 | .ghx-column-headers .ghx-column { 289 | border-bottom-width: 1px !important; 290 | padding: 0 10px 0 0 !important; 291 | } 292 | 293 | .ghx-controls-list>dl { 294 | margin-right: 0; 295 | } 296 | 297 | input.filter { 298 | margin-right: 5px; 299 | } 300 | 301 | /* 302 | input.filter .z { 303 | background: #fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAb0lEQVR42p2RMQ7AIAhFWXsG1h7Gtffo6urqyJGpwyetomnwJ38BH/wgfZSaS7PABbVOqkqm3Fzx6IQTatlDb5PJi21YBy1iuNgjJIgzFXqyCcXj1Z1DXNGTiw01aP252DCCgH7Fk41h8KaAGMDxADnaOPucd/m3AAAAAElFTkSuQmCC) no-repeat 7px 6px; 304 | margin: 8px 0; 305 | padding: 2px 10px 2px 25px; 306 | } 307 | 308 | input.filter:focus { 309 | background-color: #fff; 310 | outline: none; 311 | } 312 | */ 313 | 314 | .ghx-controls-sprint dd a>span { 315 | padding-top: 0; 316 | padding-bottom: 0; 317 | } 318 | 319 | .ghx-controls-filters dt, 320 | .ghx-controls-filters dd { 321 | margin: 0 0 2px 0 !important; 322 | } 323 | 324 | .ghx-controls-filters dd a { 325 | padding: 4px 10px; 326 | } 327 | 328 | .ghx-quick-content.aui-expander-content { 329 | margin-bottom: 0; 330 | } 331 | 332 | .ghx-controls-plan, 333 | .ghx-header-compact .ghx-controls-report, 334 | .ghx-controls-work { 335 | min-height: initial; 336 | } 337 | 338 | .ghx-column-headers .ghx-release { 339 | font-size: 9px; 340 | } 341 | 342 | .ghx-feedback.js-feedback-link { 343 | display: none; 344 | } 345 | 346 | .ghx-qty { 347 | font-weight: normal; 348 | font-size: 9px; 349 | background-color: #f5f5f5; 350 | border-radius: 3px; 351 | padding: 2px 4px 0 4px; 352 | } 353 | 354 | .ghx-column-headers h2 { 355 | float: left; 356 | } 357 | 358 | .ghx-issue-fields .ghx-type { 359 | left: 5px; 360 | top: 3px; 361 | } 362 | 363 | .ghx-has-avatar .ghx-issue-fields, 364 | .ghx-has-corner .ghx-issue-fields { 365 | padding-right: 0 !important; 366 | } 367 | 368 | .ghx-band-1 .ghx-avatar.avatar-improved, 369 | .ghx-band-2 .ghx-avatar.avatar-improved, 370 | .ghx-avatar.avatar-improved, 371 | .ghx-issue .ghx-avatar.avatar-improved { 372 | float: right !important; 373 | margin-right: 3px !important; 374 | margin-left: 2px !important; 375 | position: relative !important; 376 | top: 0 !important; 377 | right: 0 !important; 378 | left: 0 !important; 379 | } 380 | 381 | .ghx-swimlane-header { 382 | font-size: 12px; 383 | line-height: 0.4em; 384 | overflow: visible; 385 | } 386 | 387 | .ghx-swimlane-header .ghx-expander .ghx-iconfont { 388 | margin-top: 6px !important; 389 | } 390 | 391 | 392 | .ghx-issue .ghx-flags { 393 | left: 5px; 394 | position: absolute; 395 | top: 22px; 396 | opacity: 0.3; 397 | } 398 | 399 | .ghx-issue:hover .ghx-flags { 400 | opacity: 1; 401 | } 402 | 403 | .ghx-issue .ghx-corner, 404 | .ghx-issue .ghx-corner .aui-badge{ 405 | min-width: 1ch !important; 406 | } 407 | 408 | .ghx-issue .ghx-end { 409 | right: 4px !important; 410 | bottom: 5px !important; 411 | box-shadow: none !important; 412 | -webkit-box-shadow: none !important;; 413 | background: transparent !important;; 414 | } 415 | 416 | .ghx-subtask-group { 417 | padding-left: 6px; 418 | } 419 | 420 | .aui-badge { 421 | background: #f5f5f5 !important;; 422 | } 423 | 424 | 425 | .ghx-swimlane-header .ghx-heading { 426 | display: inline-block; 427 | margin: 10px 10px 10px 0; 428 | width: auto; 429 | } 430 | 431 | .ghx-swimlane-header .improved { 432 | display: inline-block; 433 | } 434 | 435 | /* 436 | Because of duplication. This is probably not a great long-term solution. 437 | */ 438 | .ghx-highlighted-fields { 439 | display: none; 440 | } 441 | -------------------------------------------------------------------------------- /src/images/github-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/github-16.png -------------------------------------------------------------------------------- /src/images/github-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/github-32.png -------------------------------------------------------------------------------- /src/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/icon-128.png -------------------------------------------------------------------------------- /src/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/icon-16.png -------------------------------------------------------------------------------- /src/images/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/icon-19.png -------------------------------------------------------------------------------- /src/images/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylang/jira-improved/8a16d011544a1ac0f077e2d5783ec8c18d6b365a/src/images/icon-38.png -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | chrome.runtime.onInstalled.addListener(function (details) { 5 | //console.log('previousVersion', details.previousVersion); 6 | }); 7 | 8 | chrome.tabs.onUpdated.addListener(function (tabId) { 9 | //chrome.pageAction.show(tabId); 10 | }); 11 | console.log('BACKGROUND Event Page for Page Action'); 12 | 13 | */ 14 | 15 | -------------------------------------------------------------------------------- /src/js/content-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var head = document.getElementsByTagName('head')[0]; 4 | 5 | var link = document.createElement('link'); 6 | link.type = 'text/css'; 7 | link.rel = 'stylesheet'; 8 | link.href = chrome.extension.getURL('/css/improved.css'); 9 | 10 | head.appendChild(link); 11 | 12 | var script = document.createElement('script'); 13 | script.src = chrome.extension.getURL('/js/dist/bundle.js'); 14 | 15 | head.appendChild(script); 16 | -------------------------------------------------------------------------------- /src/js/content/customfields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cache = require('lscache'); 4 | const _ = require('lodash'); 5 | const api = require('./util/api'); 6 | 7 | // TODO - figure these out automatically 8 | //const CUSTOM_FIELD_EPIC_NAME = 'customfield_13259'; 9 | const CUSTOM_FIELD_PULL_REQUESTS = 'customfield_13153'; 10 | 11 | let epicLink; 12 | 13 | function getEpicLink() { 14 | return epicLink; 15 | } 16 | 17 | function* guessEpicLinkCustomField() { 18 | 19 | if (getEpicLink()) { 20 | epicLink = cache.get('customField:EPIC_PARENT'); 21 | return; 22 | } 23 | 24 | const query = { 25 | startAt: 0, 26 | maxResults: 1, 27 | jql: 'issueFunction in linkedIssuesOf("resolution = unresolved", "is Epic of")' 28 | }; 29 | 30 | let data; 31 | try { 32 | data = yield api.jql(query); 33 | } catch (err) { 34 | console.log('Improved: Bad JQL request for custom fields', query, err.message, err); 35 | return; 36 | } 37 | 38 | 39 | if (!data || !data.issues) { 40 | return; 41 | } 42 | 43 | const epicLinkCustomIdArray = data.issues.map(function(issue) { 44 | const epicLinkCustomId = _(data.issues[0].fields) 45 | .findKey(function(value, key){ 46 | 47 | return key.startsWith('customfield_') && 48 | _.isString(value) && 49 | value.includes('-') && 50 | value.match(/^[A-Z]+-[0-9]+$/); 51 | }); 52 | 53 | if (!epicLinkCustomId) { 54 | console.log('Jira Improved: No EPIC PARENT for prefix:', issue.key); 55 | } 56 | 57 | return epicLinkCustomId; 58 | 59 | }).filter(Boolean); 60 | 61 | if (_.isArray(epicLinkCustomIdArray) && epicLinkCustomIdArray[0]) { 62 | epicLink = epicLinkCustomIdArray[0]; 63 | cache.set('customField:EPIC_PARENT', epicLink); 64 | console.log('Jira Improved: Found EPIC_PARENT:', epicLink); 65 | } else { 66 | console.log('Jira Improved: EPIC SAD: Could not guess the custom field for EPIC PARENT.'); 67 | } 68 | 69 | //epicLink = epicLinkCustomId; 70 | 71 | } 72 | 73 | function* init() { 74 | yield [guessEpicLinkCustomField]; 75 | } 76 | 77 | module.exports = { 78 | init: init, 79 | epicLink: getEpicLink, 80 | //EPIC_NAME: CUSTOM_FIELD_EPIC_NAME, 81 | PULL_REQUESTS: CUSTOM_FIELD_PULL_REQUESTS 82 | }; 83 | -------------------------------------------------------------------------------- /src/js/content/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var manifest = require('../../manifest.json'); 4 | 5 | const page = require('./page'); 6 | const GH = page.GH; 7 | 8 | function runExtension() { 9 | console.log('(((============ JIRA IMPROVED ' + manifest.version + ' ADDED ==============)))'); 10 | 11 | const cache = require('lscache'); 12 | 13 | const CACHE_VERSION = 3; 14 | if (cache.get('CACHE_VERSION') !== CACHE_VERSION) { 15 | console.log('Jira Improved', 'Old cache version:', cache.get('CACHE_VERSION'), 'flushing to use', CACHE_VERSION); 16 | cache.flush(); 17 | cache.set('CACHE_VERSION', CACHE_VERSION); 18 | } 19 | 20 | const epicTickets = require('./tickets/epic-tickets'); 21 | const issueTickets = require('./tickets/issue-tickets'); 22 | 23 | const filter = require('./ui/filter'); 24 | const avatar = require('./ui/avatar'); 25 | 26 | const customFields = require('./customfields'); 27 | const co = require('co'); 28 | 29 | function init () { 30 | co(function* () { 31 | console.log('Jira Improved: Calling Init'); 32 | avatar.update(); 33 | // make sure this is using the same data 34 | let data = yield GH.WorkDataLoader.getData(page.rapidViewID); 35 | 36 | yield customFields.init(); 37 | 38 | let rapidViewId = data.rapidViewId; 39 | cache.setBucket('improved:' + rapidViewId); 40 | 41 | epicTickets.decorate(data); 42 | issueTickets.decorate(data); 43 | 44 | // must re-register in case of updates 45 | page.changed(init); 46 | }); 47 | } 48 | 49 | function update () { 50 | console.log('Jira Improved: Calling Update'); 51 | avatar.update(); 52 | epicTickets.update(); 53 | issueTickets.update(); 54 | page.changed(init); 55 | } 56 | 57 | if (GH && GH.SwimlaneView && GH.SwimlaneView.rerenderCellOfIssue) { 58 | var original = {}; 59 | original['GH.SwimlaneView.rerenderCellOfIssue'] = GH.SwimlaneView.rerenderCellOfIssue; 60 | 61 | GH.SwimlaneView.rerenderCellOfIssue = function(key) { 62 | console.log('issue updated:', key); 63 | original['GH.SwimlaneView.rerenderCellOfIssue'](key); 64 | update(); 65 | }; 66 | } 67 | 68 | avatar.update(); 69 | filter.init(); 70 | page.changed(init); 71 | 72 | } 73 | 74 | if (GH) { 75 | runExtension(); 76 | } 77 | -------------------------------------------------------------------------------- /src/js/content/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fromQueryString = require('./util/fromQueryString'); 4 | var rapidViewID = fromQueryString('rapidView'); 5 | 6 | var AJS = window.AJS; 7 | var GH = window.GH; 8 | var $ = AJS && AJS.$; 9 | 10 | var enabled; 11 | 12 | if (rapidViewID && 13 | AJS && 14 | GH && 15 | GH.CallbackManager && 16 | $) { 17 | enabled = true; 18 | } 19 | 20 | // gh.work.pool.rendered 21 | 22 | function changed(fn) { 23 | if (enabled) { 24 | GH.CallbackManager.registerCallback(GH.WorkController.CALLBACK_POOL_RENDERED, 'SelectMostAppropriateIssueCallback', fn); 25 | } 26 | } 27 | 28 | module.exports = { 29 | AJS: AJS, 30 | GH: GH, 31 | $: $, 32 | rapidViewID: rapidViewID, 33 | changed: changed 34 | }; 35 | -------------------------------------------------------------------------------- /src/js/content/tickets/epic-tickets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const co = require('co'); 3 | const _ = require('lodash'); 4 | 5 | const escape = require('escape-html'); 6 | 7 | const customFields = require('../customfields'); 8 | const api = require('../util/api'); 9 | const page = require('../page'); 10 | const $ = page.$; 11 | const filter = require('../ui/filter'); 12 | const randomRGB = require('../util/randomRGB'); 13 | 14 | const cache = require('lscache'); 15 | 16 | let issues; 17 | let fixVersionsCache = {}; 18 | let projects; 19 | 20 | function isEpic(issue) { 21 | return issue && (issue.typeName === 'Feature' || issue.typeName === 'Epic'); 22 | } 23 | 24 | function projectOfIssue(issue) { 25 | return issue.key.replace(/-.*/, ''); 26 | } 27 | 28 | function getProjects(issues) { 29 | return _(issues).map(function(issue){ 30 | if (!isEpic(issue)) { 31 | return; 32 | } 33 | 34 | return projectOfIssue(issue); 35 | }).compact().unique().valueOf(); 36 | } 37 | 38 | function buildIssue(issue) { 39 | return '
' + 44 | issue.status + ' - ' + issue.key + ' - ' + escape(issue.summary) + '
'; 45 | } 46 | 47 | function buildIssues(issues) { 48 | return _(issues) 49 | .values() 50 | .sortByAll(['status', 'key']) 51 | .map(buildIssue) 52 | .join('\n'); 53 | } 54 | 55 | function buildLight(colors, color) { 56 | return '' + 57 | (_.keys(colors[color]).length || '') + 58 | ''; 59 | } 60 | function buildFixVersion(issueKey) { 61 | let fixVersions = fixVersionsCache[issueKey]; 62 | 63 | if (!fixVersions || !fixVersions.length) { 64 | return ''; 65 | } 66 | 67 | let fixVersionHTML = 'fixVersion fix version fixversion' + 68 | fixVersions.map(function(fixVersion) { 69 | return '' + fixVersion.name + ''; 70 | }).join(''); 71 | 72 | return fixVersionHTML; 73 | } 74 | 75 | function renderTickets(epics) { 76 | 77 | if (!epics) { return; } 78 | 79 | _.map(epics, function(colors, epicKey){ 80 | 81 | let $featureTicket = $('[data-issue-key=' + epicKey + ']').first(); 82 | let $improved = $featureTicket.find('.improved'); 83 | if (!$improved.length) { 84 | $improved = $('
').appendTo($featureTicket); 85 | } 86 | 87 | $improved 88 | .html( 89 | '
' + 90 | buildFixVersion(epicKey) + 91 | '
' + 92 | '
' + 93 | buildLight(colors, 'blue-gray') + 94 | buildLight(colors, 'yellow') + 95 | buildLight(colors, 'green') + 96 | '
' + 97 | '
' + 98 | '
' + buildIssues(colors['blue-gray']) + '
' + 99 | '
' + buildIssues(colors.yellow) + '
' + 100 | '
' + buildIssues(colors.green) + '
' + 101 | '
'); 102 | }).valueOf(); 103 | } 104 | 105 | function renderFixVersions() { 106 | // get fix versions 107 | let issueKeys = _(issues).map(function(issue){ 108 | if (issue.fixVersions.length) { 109 | return issue.key; 110 | } 111 | }).compact().valueOf(); 112 | 113 | if (!issueKeys.length) { 114 | return; 115 | } 116 | 117 | const query = { 118 | maxResults: 500, 119 | jql: 'issue=' + issueKeys.join(' OR issue='), 120 | fields: ['labels', 'fixVersions'].join(',') 121 | }; 122 | 123 | co(function* (){ 124 | let data = yield api.jql(query); 125 | 126 | if (!data || !data.issues) { 127 | return; 128 | } 129 | 130 | data.issues.forEach(function(issue){ 131 | let $featureTicket = $('[data-issue-key=' + issue.key + ']'); 132 | let $improved = $featureTicket.find('.improved'); 133 | if (!$improved.length) { 134 | $improved = $('
').appendTo($featureTicket); 135 | } 136 | 137 | let $fixVersionContainer = $improved.find('.fixVersion-container'); 138 | if (!$fixVersionContainer.length) { 139 | $fixVersionContainer = $('
').appendTo($improved); 140 | } 141 | 142 | fixVersionsCache[issue.key] = fixVersionsCache[issue.key] || issue.fields.fixVersions; 143 | $fixVersionContainer.html(buildFixVersion(issue.key)); 144 | 145 | }); 146 | }); 147 | 148 | } 149 | 150 | function processIssues(issues) { 151 | return _.transform(issues, function(result, issue) { 152 | let parentKey = issue.fields[customFields.epicLink()]; 153 | let color = issue.fields.status.statusCategory.colorName; 154 | 155 | result[parentKey] = result[parentKey] || {}; 156 | result[parentKey][color] = result[parentKey][color] || {}; 157 | 158 | result[parentKey][color][issue.key] = { 159 | key: issue.key, 160 | color: color, 161 | summary: issue.fields.summary, 162 | status: issue.fields.status.name, 163 | cacheDate: new Date() 164 | }; 165 | 166 | return result; 167 | }, {}).valueOf(); 168 | } 169 | 170 | function getTickets(project, startAt, acc) { 171 | if (!customFields.epicLink()) { 172 | console.log('Jira Improved: Cannot show Epic issues without knowning the customfield for EPIC LINK.'); 173 | return; 174 | } 175 | 176 | let query = { 177 | startAt: startAt || 0, 178 | maxResults: 100, 179 | fields: ['summary', 'status', customFields.epicLink()].join(','), 180 | jql: 'issueFunction in linkedIssuesOf("' + 181 | 'project = \'' + project + '\' AND ' + 182 | 'resolution = unresolved", "is Epic of")' 183 | }; 184 | 185 | co(function* () { 186 | let data = yield api.jql(query); 187 | 188 | if (!data || !data.issues) { 189 | return; 190 | } 191 | 192 | let allProcessedData = _.merge(acc, processIssues(data.issues), {}); 193 | 194 | if (data.total > (data.maxResults + data.startAt)) { 195 | if (!cache.get('linkedTickets')) { 196 | renderTickets(allProcessedData); 197 | filter.filter(true); 198 | } 199 | 200 | return getTickets(project, data.maxResults + data.startAt, allProcessedData); 201 | } 202 | 203 | renderTickets(allProcessedData); 204 | filter.filter(true); 205 | cache.set('linkedTickets:' + project, allProcessedData); 206 | //console.log('Jira Improved: all tickets complete for ' + project, allProcessedData); 207 | }).catch(function(err) { console.error(err.message, err); }); 208 | } 209 | 210 | function update(){ 211 | if (!projects.length) { 212 | return; 213 | } 214 | 215 | _.each(projects, function(project){ 216 | let previousData = cache.get('linkedTickets:' + project); 217 | renderTickets(previousData); 218 | getTickets(project, 0, {}); 219 | }); 220 | renderFixVersions(); 221 | } 222 | 223 | 224 | 225 | function decorate(data) { 226 | issues = data.issuesData.issues; 227 | projects = getProjects(issues); 228 | 229 | console.log('===== all projects ===== ', projects.join(',')); 230 | 231 | update(); 232 | } 233 | 234 | module.exports = { 235 | decorate: decorate, 236 | update: update 237 | }; 238 | -------------------------------------------------------------------------------- /src/js/content/tickets/issue-tickets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | var co = require('co'); 5 | 6 | const escape = require('escape-html'); 7 | 8 | const CUSTOMFIELDS = require('../customfields'); 9 | const FIELDS = ['summary', 'status']; //, CUSTOMFIELDS.EPIC_NAME]; 10 | 11 | const api = require('../util/api'); 12 | const page = require('../page'); 13 | const $ = page.$; 14 | 15 | const findPRs = require('../util/findPRs'); 16 | const randomRGB = require('../util/randomRGB'); 17 | 18 | const cache = require('lscache'); 19 | 20 | const STATUS_MAP = { 21 | Open: 'Backlog' 22 | }; 23 | 24 | let epics; 25 | let openTicketsToCheckForPRs; 26 | 27 | function renderPullRequests(issueKey, prString) { 28 | 29 | if (!issueKey || !prString) { 30 | return; 31 | } 32 | 33 | const $issue = $('[data-issue-key=' + issueKey + ']:not(.ghx-swimlane-header):not(.ghx-parent-group)'); 34 | 35 | $issue.find('.pull-requests').remove(); 36 | 37 | var pullRequests = findPRs(prString); 38 | 39 | if (pullRequests) { 40 | var $where = $('
').appendTo($issue); 41 | 42 | pullRequests.forEach(function(pullRequest) { 43 | 44 | if (pullRequest.api) { 45 | $('' + 47 | '').tipsy({opacity: 1}) 48 | .appendTo($where); 49 | 50 | api.get(pullRequest.api).then(function(data) { 51 | if (!data) { return; } 52 | 53 | $('[data-pr="' + pullRequest.api + '"') 54 | .removeClass('pull-request-unknown') 55 | .addClass('pull-request-' + data.state); 56 | }).fail(function(err) { 57 | 58 | console.log(issueKey, pullRequest.api, err); 59 | 60 | if (err.status === 0) { return; } 61 | 62 | $('[data-pr="' + pullRequest.api + '"') 63 | .removeClass('pull-request-unknown') 64 | .addClass('pull-request-error'); 65 | }); 66 | } 67 | 68 | if (pullRequest.favIcon) { 69 | $where.append('' + 71 | '' + 72 | '' + pullRequest.host.substring(0, 2) +'' + 73 | ''); 74 | } 75 | }); 76 | } 77 | } 78 | 79 | function renderLabels(issueKey, labels) { 80 | 81 | if (!labels || !labels.length) { 82 | return; 83 | } 84 | 85 | const $issueKey = $('.ghx-issue[data-issue-key=' + issueKey + ']'); 86 | let $labelContainer = $issueKey.find('.labelContainer'); 87 | 88 | if (!$labelContainer.length) { 89 | $labelContainer = $('
').appendTo($issueKey); 90 | } 91 | 92 | $labelContainer.html( 93 | 'labels' + 94 | labels.map(function(label) { 95 | return '' + label + ''; 96 | }).join('')); 97 | } 98 | 99 | 100 | function renderEpic(epicId) { 101 | 102 | var epic = cache.get(epicId); 103 | 104 | if (!epic || !epic.issueKeys) { 105 | console.log('Jira Improved: No cache for', epicId, epic); 106 | return; 107 | } 108 | 109 | var issueKeyFilter = epic.issueKeys.map(function(issueKey) { 110 | return '[data-issue-key=' + issueKey + ']'; 111 | }).join(', '); 112 | 113 | $(issueKeyFilter) 114 | //.find('.ghx-summary') 115 | .filter('.ghx-issue') // only issues, not parents of structure issues 116 | .not(':has(.epic-container)') 117 | //.not(':has([data-epickey="' + epicId + '")]') 118 | .append(''); 129 | 130 | 131 | var $epicLinks = $('.originalColor[data-epic="' + epicId + '"]'); 132 | if (!$epicLinks.length) { 133 | return; 134 | } 135 | var rgbColor = $epicLinks.css('backgroundColor'); 136 | var randomColor = randomRGB(rgbColor, epic.summary); 137 | $epicLinks 138 | .removeClass('originalColor') 139 | .css('backgroundColor', randomColor) 140 | .tipsy(); 141 | } 142 | 143 | function updateEpicCache() { 144 | _.forEach(epics, function(issueKeys, epicId) { 145 | 146 | co(function* (){ 147 | let data = yield api.issue(epicId, FIELDS); 148 | 149 | if (!data || !data.fields || !data.fields.status || !data.fields.summary) { return; } 150 | 151 | let summary = data.fields.summary.trim(); 152 | /*let alternateSummary = (data.fields[CUSTOMFIELDS.EPIC_NAME] || summary).trim(); 153 | 154 | if (summary.toLocaleLowerCase() !== alternateSummary.toLocaleLowerCase()) { 155 | console.log('Mismatched names!'); 156 | console.log(epicId, ' > ', data.fields.summary); 157 | console.log(epicId, ' > ', data.fields[CUSTOMFIELDS.EPIC_NAME]); 158 | if (alternateSummary.length < summary.length) { 159 | [ summary, alternateSummary ] = [ alternateSummary, summary ]; 160 | } 161 | } else { 162 | alternateSummary = ''; 163 | }*/ 164 | 165 | cache.set(epicId, { 166 | status: STATUS_MAP[data.fields.status.name] || data.fields.status.name, 167 | statusId: data.fields.status.statusCategory.id, 168 | summary: summary, 169 | //alternateSummary: alternateSummary, 170 | issueKeys: issueKeys 171 | }); 172 | 173 | renderEpic(epicId); 174 | }); 175 | }); 176 | } 177 | 178 | function checkDevStatusForPullRequests(){ 179 | 180 | console.log('Jira Improved: checkDevStatusForPullRequests'); 181 | 182 | if (!openTicketsToCheckForPRs || !openTicketsToCheckForPRs.length) { 183 | return; 184 | } 185 | 186 | co(function* () { 187 | 188 | const pullRequestsSparse = yield _.map(openTicketsToCheckForPRs, function* (issue){ 189 | const prStatusUrl = `/rest/dev-status/1.0/issue/detail?issueId=${ issue.id }&applicationType=githubenterprise&dataType=pullrequest`; 190 | 191 | let data = yield api.get(prStatusUrl); 192 | 193 | if (!data || !data.detail || !data.detail[0].pullRequests) { 194 | return; 195 | } 196 | 197 | const pullRequests = _.map(data.detail[0].pullRequests, function(pullRequest) { 198 | return pullRequest.url; 199 | }).join(' '); 200 | 201 | if (!pullRequests) { 202 | return; 203 | } 204 | 205 | return { 206 | key: issue.key, 207 | pullRequests: pullRequests 208 | }; 209 | }); 210 | 211 | const pullRequests= _.compact(pullRequestsSparse); 212 | 213 | if (pullRequests.length) { 214 | //$('.pull-requests').remove(); 215 | 216 | _.forEach(pullRequests, function(issue){ 217 | renderPullRequests(issue.key, issue.pullRequests); 218 | }); 219 | 220 | cache.set('hasDevStatusPRs', true); 221 | cache.set('pull-requests', pullRequests); 222 | } else { 223 | cache.remove('hasDevStatusPRs'); 224 | cache.remove('pull-requests'); 225 | } 226 | }); 227 | } 228 | 229 | function checkCustomFieldForPullRequests(ticketsToCheck = openTicketsToCheckForPRs){ 230 | 231 | if (!ticketsToCheck || !ticketsToCheck.length) { 232 | return; 233 | } 234 | 235 | console.log('Jira Improved: checkCustomFieldForPullRequests', ticketsToCheck.length); 236 | 237 | const issues = _.pluck(ticketsToCheck, 'key') 238 | .splice(0, 20) 239 | .join(' OR issue='); 240 | const query = { 241 | maxResults: 500, 242 | jql: 'issue=' + issues + ' AND ' + 243 | // move detection to separate module 244 | (document.location.hostname === 'ticket.opower.com' ? '(labels is not EMPTY OR "Code Review URL(s)" is not EMPTY)' : 245 | 'labels is not EMPTY'), 246 | fields: ['labels', 'description', CUSTOMFIELDS.PULL_REQUESTS].join(',') 247 | }; 248 | 249 | co(function* (){ 250 | let data = yield api.jql(query); 251 | 252 | if (!data || !data.issues) { 253 | return; 254 | } 255 | 256 | const labels = _(data.issues) 257 | .map(function(issue){ 258 | const labels = issue.fields.labels; 259 | 260 | if (!labels || !labels.length) { 261 | return; 262 | } 263 | 264 | return { 265 | key: issue.key, 266 | labels: labels 267 | }; 268 | }) 269 | .compact() 270 | .valueOf(); 271 | 272 | const pullRequests = _(data.issues) 273 | .map(function(issue){ 274 | //const pullRequests = issue.fields.description; //[CUSTOMFIELDS.PULL_REQUESTS]; 275 | const pullRequests = issue.fields[CUSTOMFIELDS.PULL_REQUESTS]; 276 | 277 | if (!pullRequests) { 278 | return; 279 | } 280 | 281 | return { 282 | key: issue.key, 283 | pullRequests: pullRequests 284 | }; 285 | }) 286 | .compact() 287 | .valueOf(); 288 | 289 | if (labels.length) { 290 | //$('.improved-labels').remove(); 291 | 292 | _.forEach(labels, function(issue){ 293 | renderLabels(issue.key, issue.labels); 294 | }); 295 | 296 | //cache.set('pull-requests', pullRequests); 297 | //cache.set('hasCustomFieldPRs', true); 298 | } 299 | 300 | if (pullRequests.length) { 301 | //$('.pull-requests').remove(); 302 | 303 | _.forEach(pullRequests, function(issue){ 304 | renderPullRequests(issue.key, issue.pullRequests); 305 | }); 306 | 307 | cache.set('pull-requests', pullRequests); 308 | cache.set('hasCustomFieldPRs', true); 309 | } else { 310 | cache.remove('hasCustomFieldPRs'); 311 | cache.remove('pull-requests'); 312 | } 313 | 314 | }); 315 | 316 | checkCustomFieldForPullRequests(ticketsToCheck.splice(20)); 317 | } 318 | 319 | function updatePRs() { 320 | if (!openTicketsToCheckForPRs) { 321 | return; 322 | } 323 | 324 | const cachedPullRequests = cache.get('pull-requests'); 325 | 326 | _.forEach(cachedPullRequests, function(issue){ 327 | renderPullRequests(issue.key, issue.pullRequests); 328 | }); 329 | 330 | checkCustomFieldForPullRequests(); 331 | checkDevStatusForPullRequests(); 332 | } 333 | 334 | function update(){ 335 | if (epics) { 336 | // Show the cached value if there is one 337 | _.forEach(epics, function(issueKeys, epicId) { 338 | renderEpic(epicId); 339 | }); 340 | } 341 | 342 | updatePRs(); 343 | } 344 | 345 | 346 | function decorate(data) { 347 | if (!data.rapidViewId || !data.issuesData || !data.issuesData.issues) { 348 | console.log('not enough info for decorate', data); 349 | return; 350 | } 351 | 352 | const issues = _(data.issuesData.issues) 353 | .filter('key') 354 | .reject({typeName: 'Feature'}) 355 | .reject({typeName: 'Epic'}) 356 | .valueOf(); 357 | 358 | if (!issues.length) { 359 | return; 360 | } 361 | 362 | epics = _(issues) 363 | .filter('epic') 364 | .reduce(function(acc, issue){ 365 | let epic = issue.epic; 366 | let issueKey = issue.key; 367 | acc[epic] = acc[epic] || []; 368 | acc[epic].push(issueKey); 369 | return acc; 370 | }, {}) 371 | .valueOf(); 372 | 373 | openTicketsToCheckForPRs = _(issues) 374 | .filter(function(issue) { 375 | return issues.length < 100 || (issue.statusName !== 'Closed'); 376 | }) 377 | .map(function(issue){ 378 | return { 379 | id: issue.id, 380 | key: issue.key 381 | }; 382 | }) 383 | .valueOf(); 384 | 385 | update(); 386 | updateEpicCache(); 387 | } 388 | 389 | module.exports = { 390 | decorate: decorate, 391 | update: update 392 | }; 393 | -------------------------------------------------------------------------------- /src/js/content/ui/avatar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var page = require('../page'); 4 | var $ = page.$; 5 | 6 | function update() { 7 | $('.ghx-avatar').each(function(i, el){ 8 | var $avatar = $(el).addClass('avatar-improved'); 9 | var $parent = $avatar.parent('.ghx-has-avatar'); 10 | $avatar.prependTo($parent); 11 | $avatar.find('[title]').tipsy(); 12 | }); 13 | } 14 | 15 | module.exports = { 16 | update: update 17 | }; 18 | -------------------------------------------------------------------------------- /src/js/content/ui/emptyColumn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var page = require('../page'); 4 | var $ = page.$; 5 | 6 | var $lastColumns; 7 | var $lastColumnHeader; 8 | 9 | 10 | function hide() { 11 | $lastColumnHeader.hide(); 12 | $lastColumns.hide(); 13 | } 14 | 15 | function show() { 16 | $lastColumns.show(); 17 | $lastColumnHeader.show(); 18 | } 19 | 20 | function update() { 21 | // if Hide Done is clicked, hide the last column. 22 | if ($lastColumns.has('div').length === 0) { 23 | hide(); 24 | } else { 25 | show(); 26 | } 27 | 28 | page.changed(update); 29 | 30 | } 31 | 32 | function init(){ 33 | 34 | $lastColumns = $('.ghx-columns li:last-child'); 35 | $lastColumnHeader = $('.ghx-column-headers li:last-child'); 36 | 37 | $('.js-parent-drag') 38 | .draggable({ 39 | distance: 2, 40 | start: show, 41 | stop: update 42 | }); 43 | 44 | update(); 45 | } 46 | 47 | module.exports = { 48 | init: init 49 | }; 50 | -------------------------------------------------------------------------------- /src/js/content/ui/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var page = require('../page'); 4 | var $ = page.$; 5 | 6 | var previousFilter; 7 | var previousItems; 8 | 9 | 10 | //var debounce = require('../util/debounce'); 11 | var sizzleCustomizations = require('../util/sizzle-customizations'); 12 | 13 | var $filter = $('').addClass('filter').attr('placeholder', 'Filter'); 14 | 15 | 16 | function filter() { 17 | 18 | // need to re-find all the issues in case some filter was changed that altered what tickets are viewable 19 | //.ghx-swimlane.ghx-closed .ghx-columns .ghx-issue, 20 | //.ghx-swimlane.ghx-closed .ghx-columns .ghx-parent-group { 21 | 22 | var $items = $('.ghx-parent-group, .ghx-issue'); 23 | var value = $filter.val().trim(); 24 | 25 | //if (!force && value === previousFilter && previousItems === $items.length) { 26 | // console.log('????????????? force not true, leaving'); 27 | // return; 28 | //} 29 | 30 | var $matches = $items.has(':containsAnywhere("' + value + '")'); 31 | 32 | $items.find('.highlight').removeClass('highlight'); 33 | 34 | if (value) { 35 | var searchFor = value.split(' '); 36 | 37 | searchFor.forEach(function(val){ 38 | $matches.find(':containsAnywhere("' + val + '"):not(:has(*))').addClass('highlight'); 39 | }); 40 | } 41 | 42 | $items.not($matches).addClass('hidden'); 43 | $matches.removeClass('hidden'); 44 | previousFilter = value; 45 | previousItems = $items.length; 46 | 47 | page.changed(filter); 48 | } 49 | 50 | function init() { 51 | 52 | var $whereToPutFilter = $('#js-quickfilters-label'); 53 | 54 | if (!$whereToPutFilter.length) { 55 | setTimeout(init, 100); 56 | return; 57 | } 58 | 59 | // Add new jQuery filter for finding text anywhere 60 | sizzleCustomizations.addContainsAnywhere(); 61 | 62 | // 63 | 64 | $filter 65 | .on('keyup change', filter) 66 | .replaceAll('#js-quickfilters-label'); 67 | 68 | // Hotkey for `f`/ 'F' 69 | $(window.document).bind('keyup', function(e) { 70 | var KEY_F = 102; 71 | var KEY_f = 70; 72 | 73 | var $target = $(e.target); 74 | if (page.AJS.keyboardShortcutsDisabled || $target.is(':input')) { 75 | return; 76 | } 77 | 78 | if (e.which === KEY_F || e.which === KEY_f) { 79 | $filter.focus(); 80 | e.preventDefault(); 81 | return false; 82 | } 83 | }); 84 | } 85 | 86 | module.exports = { 87 | init: init, 88 | filter: filter 89 | }; 90 | -------------------------------------------------------------------------------- /src/js/content/util/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //const got = require('got'); 4 | 5 | var API_PREFIX = window.location.pathname.replace('/secure/RapidBoard.jspa', ''); 6 | var API_URL = API_PREFIX + '/rest/api/2/'; 7 | 8 | var page = require('../page'); 9 | var $ = page.$; 10 | var querystring = require('querystring'); 11 | 12 | function get(url) { 13 | /* 14 | return got.get(url, { 15 | json: true 16 | }) 17 | .catch(err => { console.log('error getting url', url, err.message, err); return {}; }) 18 | .then(response => response.body); 19 | */ 20 | 21 | return $.ajax(url, 22 | { 23 | dataType: 'json', 24 | type: 'GET', 25 | cache: true 26 | }); 27 | } 28 | 29 | function jql(query) { 30 | return get(API_URL + 'search?' + querystring.stringify(query)); 31 | } 32 | 33 | function issue(issue_id, fields) { 34 | return get(API_URL + 'issue/' + issue_id + '?' + querystring.stringify({fields: fields.join(',')})); 35 | } 36 | 37 | module.exports = { 38 | get: get, 39 | jql: jql, 40 | issue: issue 41 | }; 42 | -------------------------------------------------------------------------------- /src/js/content/util/caching.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/js/content/util/favicon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | 5 | function faviconUrl (urlStr) { 6 | 7 | var urlObj = url.parse(urlStr); 8 | 9 | var hostname = url.format({ 10 | protocol: urlObj.protocol, 11 | host: urlObj.host, 12 | port: urlObj.port 13 | }); 14 | 15 | var backupFavIcon = url.format({ 16 | protocol: urlObj.protocol, 17 | host: urlObj.host, 18 | port: urlObj.port, 19 | pathname: 'favicon.ico' 20 | }); 21 | 22 | if (urlObj.host === 'src.va.opower.it') { 23 | return 'http://marketplace.servicerocket.com/static/products/atlassian/logoCruciblePNG.png'; 24 | } 25 | 26 | if (urlObj.host === 'testrail.va.opower.it') { 27 | return 'http://help.fogcreek.com/wp-content/uploads/2015/01/testrail.png'; 28 | } 29 | 30 | 31 | 32 | /* 33 | var favIconNotWorking = url.format({ 34 | protocol: 'https', 35 | host: 'getfavicon.appspot.com', 36 | pathname: hostname, 37 | search: 'defaulticon=' + backupFavIcon 38 | }); 39 | */ 40 | 41 | //https://favatar.mention.com/ 42 | var favIcon = url.format({ 43 | protocol: 'https', 44 | host: 'favatar.mention.com', 45 | pathname: 'image', 46 | query: { 47 | url: hostname, 48 | format: 'image', 49 | api_key: 'favatardemokey', 50 | default: backupFavIcon 51 | } 52 | }); 53 | 54 | 55 | return favIcon; 56 | } 57 | 58 | module.exports = faviconUrl; 59 | -------------------------------------------------------------------------------- /src/js/content/util/findPRs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getUrls = require('get-urls'); 4 | var url = require('url'); 5 | var path = require('path'); 6 | var favIconUrl = require('./favicon'); 7 | 8 | function prUrlToAPI(str) { 9 | if (!str || str.length === 0) { return; } 10 | 11 | var urlSet = getUrls(str); 12 | var urls = Array.from(urlSet); 13 | 14 | // URL: https://github.com/x-web/x-web-canonical-lookup/pull/35 15 | // API: https://github.com/api/v3/repos/x-web/x-web-canonical-lookup/pulls/35 16 | 17 | 18 | return urls.map(function(urlString){ 19 | 20 | var urlObj = url.parse(urlString); 21 | 22 | var apiPath = urlObj.pathname.split('/').slice(1,5); 23 | var apiUrl; 24 | var favIcon; 25 | 26 | if (apiPath[2] === 'pull') { 27 | 28 | apiPath[2] = 'pulls'; 29 | 30 | apiUrl = url.format({ 31 | protocol: urlObj.protocol, 32 | host: urlObj.host, 33 | port: urlObj.port, 34 | pathname: path.join('/api/v3/repos', path.join.apply(path, apiPath)) 35 | }); 36 | } else { 37 | //http://g.etfv.co/http://www.google.com?defaulticon=http://en.wikipedia.org/favicon.ico 38 | favIcon = favIconUrl(urlString); 39 | } 40 | 41 | return { 42 | url: urlString, 43 | api: apiUrl, 44 | host: urlObj.host, 45 | favIcon: favIcon 46 | }; 47 | }); 48 | } 49 | 50 | module.exports = prUrlToAPI; 51 | -------------------------------------------------------------------------------- /src/js/content/util/fromQueryString.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var querystring = require('querystring'); 4 | 5 | function fromQueryString (key) { 6 | var qs = window.location.search.substring(1); 7 | return querystring.parse(qs)[key]; 8 | } 9 | 10 | module.exports = fromQueryString; 11 | -------------------------------------------------------------------------------- /src/js/content/util/randomRGB.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var seedRandom = require('seed-random'); 4 | 5 | var MAX_COLORS = 256; 6 | var RANDOM_RANGE = 0.3; 7 | var MAX_RANGE = Math.floor(MAX_COLORS * RANDOM_RANGE); 8 | 9 | function randomWithinRange(start, randomFn, range) { 10 | var min = Math.max(start - range, 0); 11 | var max = Math.min(start + range, 255); 12 | return Math.floor(randomFn() * (max - min + 1)) + min; 13 | } 14 | 15 | function toArray(str) { 16 | var arr = str.split( ',' ); 17 | return [ 18 | parseInt(arr[0].substring(4)), 19 | parseInt(arr[1]), 20 | parseInt(arr[2]) 21 | ]; 22 | } 23 | 24 | function arrayToRGB(rgbArr){ 25 | return 'rgb(' + rgbArr.join(', ') + ')'; 26 | } 27 | 28 | function randomRGB(rgbStr, seed){ 29 | var random = seedRandom(seed); 30 | var rangeRemaining = MAX_RANGE; 31 | return arrayToRGB(toArray(rgbStr) 32 | .reverse() 33 | .map(function(n){ 34 | var randomColor = randomWithinRange(n, random, rangeRemaining); 35 | rangeRemaining = rangeRemaining - Math.abs(n - randomColor); 36 | return randomColor; 37 | }) 38 | .reverse() 39 | ); 40 | } 41 | 42 | module.exports = randomRGB; 43 | -------------------------------------------------------------------------------- /src/js/content/util/sizzle-customizations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var page = require('../page'); 4 | var $ = page.$; 5 | 6 | var EXPRESSION = $.expr[':']; 7 | 8 | // Ignore case and find text almost anywhere. 9 | // Normally jquery matches case and only look in innerText. 10 | function containsAnywhere(elem, i, match) { 11 | // everything should match blank 12 | if (!match[3]) { return true; } 13 | 14 | var elementText = ( 15 | (elem.textContent || elem.innerText || '') + 16 | (elem.getAttribute('href') || '') + 17 | (elem.getAttribute('title') || '') + 18 | (elem.getAttribute('original-title') || '') 19 | ).toLowerCase(); 20 | 21 | var searchFor = (match[3] || '').toLowerCase().split(' '); 22 | 23 | // return if they ALL match 24 | var matches = searchFor.reduce(function(acc, val){ 25 | return acc && elementText.indexOf(val) >= 0; 26 | }, true); 27 | 28 | return matches; 29 | } 30 | 31 | function addContainsAnywhere() { 32 | $.extend(EXPRESSION, {containsAnywhere: containsAnywhere}); 33 | } 34 | 35 | module.exports = { 36 | addContainsAnywhere: addContainsAnywhere 37 | }; 38 | -------------------------------------------------------------------------------- /src/js/live-reload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Reload client for Chrome Apps & Extensions. 4 | // The reload client has a compatibility with livereload. 5 | // WARNING: only supports reload command. 6 | 7 | 8 | var LIVERELOAD_HOST = 'localhost:'; 9 | var LIVERELOAD_PORT = 35729; 10 | var connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload'); 11 | 12 | var error; 13 | 14 | connection.onerror = function (error) { 15 | console.log('connection got error', error); 16 | error = error; 17 | }; 18 | 19 | connection.onmessage = function (e) { 20 | 21 | if (!error && e.data) { 22 | var data = JSON.parse(e.data); 23 | if (data && data.command === 'reload') { 24 | chrome.runtime.reload(); 25 | } 26 | } 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('\'Allo \'Allo! Option'); 4 | 5 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('wtf...'); 4 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "2.0.9", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "icons": { 7 | "16": "images/icon-16.png", 8 | "128": "images/icon-128.png" 9 | }, 10 | "default_locale": "en", 11 | "background": { 12 | "scripts": [ 13 | "js/background.js" 14 | ] 15 | }, 16 | "page_action": { 17 | "default_icon": { 18 | "19": "images/icon-19.png", 19 | "38": "images/icon-38.png" 20 | }, 21 | "default_title": "Jira Improved", 22 | "default_popup": "views/popup.html" 23 | }, 24 | "options_page": "views/options.html", 25 | "omnibox": { 26 | "keyword": "Jira Improved" 27 | }, 28 | "content_scripts": [{ 29 | "matches": [ 30 | "https://*/secure/RapidBoard.jspa*", 31 | "https://*/jira/secure/RapidBoard.jspa*" 32 | ], 33 | "js": [ 34 | "js/content-script.js" 35 | ], 36 | "run_at": "document_end", 37 | "all_frames": false 38 | }], 39 | "web_accessible_resources": [ 40 | "images/*.png", 41 | "css/*.css", 42 | "js/dist/*.js" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/manifest.tmpl.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "1.0.7", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "icons": { 7 | "16": "images/icon-16.png", 8 | "128": "images/icon-128.png" 9 | }, 10 | "default_locale": "en", 11 | "background": { 12 | "scripts": ["js/background.js"] 13 | }, 14 | "page_action": { 15 | "default_icon": { 16 | "19": "images/icon-19.png", 17 | "38": "images/icon-38.png" 18 | }, 19 | "default_title": "Jira Improved", 20 | "default_popup": "views/popup.html" 21 | }, 22 | "options_page": "views/options.html", 23 | "omnibox": { 24 | "keyword": "Jira Improved" 25 | }, 26 | "content_scripts": [{ 27 | "matches": ["https://*/secure/RapidBoard.jspa*", "https://*/jira/secure/RapidBoard.jspa*"], 28 | "js": ["js/content-script.js"], 29 | "run_at": "document_end", 30 | "all_frames": false 31 | }], 32 | "web_accessible_resources": ["images/*.png", "css/*.css", "js/dist/*.js"] 33 | } 34 | -------------------------------------------------------------------------------- /src/views/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Jira Improved

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Pop Up

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------