├── .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 |  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 |  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 '
'; 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 | '