├── .babelrc
├── .gitignore
├── .travis.yml
├── IconTemplate.png
├── IconTemplate@2x.png
├── auto_updater.json
├── graphics
├── auth-bg.png
├── comments.svg
├── done.gif
└── spinn-loader.svg
├── index.html
├── index.js
├── lib
├── Auth.js
├── Jira.js
└── update.js
├── media
├── Icon.icns
├── icon.png
├── logo.png
└── minira.png
├── menu.js
├── package.json
├── readme.md
├── src
└── client
│ ├── app.injector.js
│ ├── app.js
│ ├── app.routes.js
│ ├── components
│ ├── auth.component.js
│ ├── issue.component.js
│ ├── issues.component.js
│ ├── settings.component.js
│ └── suggest.component.js
│ ├── directives
│ └── authRouterOutlet.directive.js
│ ├── scss
│ ├── base
│ │ └── _base.scss
│ ├── components
│ │ ├── _btn.scss
│ │ ├── _labels.scss
│ │ └── _loading.scss
│ ├── elements
│ │ └── _logo.scss
│ ├── layout
│ │ └── _frame.scss
│ ├── modules
│ │ ├── _auth.scss
│ │ ├── _issue-comments.scss
│ │ ├── _issue.scss
│ │ ├── _issues.scss
│ │ ├── _settings.scss
│ │ └── _suggest.scss
│ └── utils
│ │ └── _colors.scss
│ ├── services
│ ├── jira.service.js
│ └── nav.service.js
│ └── templates
│ ├── app.template.html
│ ├── auth.template.html
│ ├── issue.template.html
│ ├── issues.template.html
│ └── settings.template.html
├── webpack-production.config.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": [
4 | "angular2-annotations",
5 | "transform-decorators-legacy",
6 | "transform-class-properties",
7 | "transform-flow-strip-types"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bundle.js
3 | bundle.css
4 | build
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '4.3'
4 | env:
5 | - CXX=g++-4.8
6 | addons:
7 | apt:
8 | sources:
9 | - ubuntu-toolchain-r-test
10 | packages:
11 | - g++-4.8
12 | - libgnome-keyring-dev
13 |
--------------------------------------------------------------------------------
/IconTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/IconTemplate.png
--------------------------------------------------------------------------------
/IconTemplate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/IconTemplate@2x.png
--------------------------------------------------------------------------------
/auto_updater.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "https://github.com/jenslind/minira/releases/download/v1.0.1/Minira-osx-1.0.1.zip"
3 | }
4 |
--------------------------------------------------------------------------------
/graphics/auth-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/graphics/auth-bg.png
--------------------------------------------------------------------------------
/graphics/comments.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/graphics/done.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/graphics/done.gif
--------------------------------------------------------------------------------
/graphics/spinn-loader.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const menubar = require('menubar')
4 | const Jira = require('./lib/Jira.js')
5 | const Auth = require('./lib/Auth.js')
6 | const autoUpdate = require('./lib/update.js')
7 | const ipc = require('electron').ipcMain
8 | const Menu = require('electron').Menu
9 | const storage = require('electron-json-storage')
10 | const menuTpl = require('./menu.js')
11 |
12 | const mb = menubar({
13 | 'preload-window': true,
14 | 'transparent': true,
15 | 'resizable': false
16 | })
17 | let jira = null
18 |
19 | mb.on('ready', () => {
20 | // Auto-update
21 | autoUpdate(mb.app.getVersion())
22 |
23 | // Set application menu
24 | const appMenu = Menu.buildFromTemplate(menuTpl)
25 | Menu.setApplicationMenu(appMenu)
26 |
27 | // Get many issues
28 | ipc.on('getIssues', (event, jql) => {
29 | jira.getIssues(jql)
30 | .then((issues) => mb.window.webContents.send('issues', issues))
31 | })
32 |
33 | // Get a specific jira issue
34 | ipc.on('getIssue', (event, id) => {
35 | jira.getIssue(id)
36 | .then((issue) => mb.window.webContents.send('issue', issue))
37 | })
38 |
39 | ipc.on('getComments', (event, id) => {
40 | jira.getComments(id)
41 | .then((comments) => mb.window.webContents.send('comments', comments))
42 | })
43 |
44 | ipc.on('addComment', (event, comment) => {
45 | jira.addComment(comment)
46 | .then((newComment) => mb.window.webContents.send('commentAdded', newComment))
47 | })
48 |
49 | // Get assignable users to a issue
50 | ipc.on('getAssignable', (event, issueId) => {
51 | jira.getAssignable(issueId)
52 | .then((assignable) => mb.window.webContents.send('assignable', assignable))
53 | })
54 |
55 | // Assign user to a issue
56 | ipc.on('assignUser', (event, data) => {
57 | jira.assignUser(data.issue, data.user)
58 | })
59 |
60 | ipc.on('getTransitions', (event, issueId) => {
61 | jira.getTransitions(issueId)
62 | .then((transitions) => {
63 | event.returnValue = transitions
64 | })
65 | })
66 |
67 | ipc.on('doTransition', (event, data) => {
68 | jira.transition(data.issueId, data.transitionId)
69 | })
70 |
71 | ipc.on('isAuthed', (event) => {
72 | Auth.getAuth((success) => {
73 | if (!jira && success) jira = new Jira(success)
74 | event.returnValue = success
75 | })
76 | })
77 |
78 | ipc.on('auth', (event, info) => {
79 | Auth.auth(info)
80 | .then((res) => {
81 | event.returnValue = true
82 | })
83 | .catch(() => {
84 | event.returnValue = false
85 | })
86 | })
87 |
88 | ipc.on('unAuth', (event) => {
89 | Auth.unAuth((done) => {
90 | event.returnValue = done
91 | })
92 | })
93 |
94 | ipc.on('updateSettings', (event, data) => {
95 | storage.set('settings', data)
96 | })
97 |
98 | ipc.on('getSettings', (event) => {
99 | storage.get('settings', (err, settings) => {
100 | event.returnValue = (!err) ? settings : null
101 | })
102 | })
103 | })
104 |
--------------------------------------------------------------------------------
/lib/Auth.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const keytar = require('keytar')
4 | const storage = require('electron-json-storage')
5 | const got = require('got')
6 |
7 | class Auth {
8 |
9 | static test (info) {
10 | return got(info.baseUrl + '/rest/api/2/myself', {
11 | auth: info.user + ':' + info.pass,
12 | json: true
13 | })
14 | }
15 |
16 | static auth (info) {
17 | return Auth.test(info)
18 | .then((res) => {
19 | storage.set('authSettings', {
20 | baseUrl: info.baseUrl,
21 | user: info.user,
22 | userAvatar: res.body.avatarUrls['48x48']
23 | }, () => {
24 | keytar.addPassword('Minira', info.user, info.pass)
25 | Promise.resolve(true)
26 | })
27 | })
28 | }
29 |
30 | static getAuth (cb) {
31 | storage.get('authSettings', (err, settings) => {
32 | if (!Object.keys(settings).length || err) return cb(false)
33 | const pass = keytar.getPassword('Minira', settings.user)
34 | if (!pass) return cb(false)
35 |
36 | return cb({
37 | baseUrl: settings.baseUrl,
38 | user: settings.user,
39 | pass: pass,
40 | avatar: settings.userAvatar
41 | })
42 | })
43 | }
44 |
45 | static unAuth (cb) {
46 | storage.get('authSettings', (err, settings) => {
47 | if (err) return cb(false)
48 | storage.remove('authSettings', (err) => {
49 | if (err) return cb(false)
50 | keytar.deletePassword('Minira', settings.user)
51 | cb(true)
52 | })
53 | })
54 | }
55 |
56 | }
57 |
58 | module.exports = Auth
59 |
--------------------------------------------------------------------------------
/lib/Jira.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const got = require('got')
4 |
5 | class Jira {
6 |
7 | constructor (auth) {
8 | if (auth) {
9 | this.BASE_URL = auth.baseUrl
10 | this.USER = auth.user
11 | this.PASS = auth.pass
12 | }
13 | }
14 |
15 | _getAuth () {
16 | return this.USER + ':' + this.PASS
17 | }
18 |
19 | _getBaseUrl () {
20 | return this.BASE_URL + '/rest/api/2'
21 | }
22 |
23 | getIssues (jql) {
24 | let self = this
25 |
26 | if (!jql) jql = 'assignee=' + this.USER + ' AND status!=done'
27 |
28 | return got(this._getBaseUrl() + '/search?jql=' + jql,
29 | {
30 | auth: self._getAuth(),
31 | json: true
32 | })
33 | .then((res) => {
34 | return res.body.issues
35 | })
36 | }
37 |
38 | getIssue (id) {
39 | let self = this
40 | return got(this._getBaseUrl() + '/issue/' + id + '?expand=renderedFields,transitions', {
41 | auth: self._getAuth(),
42 | json: true
43 | })
44 | .then((res) => {
45 | return res.body
46 | })
47 | }
48 |
49 | assignUser (issue, user) {
50 | let self = this
51 | return got.put(issue,
52 | {
53 | auth: self._getAuth(),
54 | body: '{ "fields": { "assignee": { "name": "' + user + '" } } }',
55 | headers: {
56 | 'Content-Type': 'application/json'
57 | }
58 | })
59 | }
60 |
61 | getAssignable (issueKey) {
62 | let self = this
63 | return got(this._getBaseUrl() + '/user/assignable/search?issueKey=' + issueKey,
64 | {
65 | auth: self._getAuth(),
66 | json: true
67 | })
68 | .then((res) => {
69 | return res.body
70 | })
71 | }
72 |
73 | getTransitions (issueId) {
74 | let self = this
75 | return got(this._getBaseUrl() + '/issue/' + issueId + '/transitions',
76 | {
77 | auth: self._getAuth(),
78 | json: true
79 | })
80 | .then((res) => {
81 | return res.body
82 | })
83 | }
84 |
85 | transition (issueId, transitionId) {
86 | let self = this
87 | return got.post(this._getBaseUrl() + '/issue/' + issueId + '/transitions', {
88 | auth: self._getAuth(),
89 | body: '{ "transition": { "id":' + transitionId + '} }',
90 | headers: {
91 | 'Content-Type': 'application/json'
92 | }
93 | })
94 | }
95 |
96 | getComments (id) {
97 | let self = this
98 | return got(this._getBaseUrl() + '/issue/' + id + '/comment?expand=renderedBody', {
99 | auth: self._getAuth(),
100 | json: true
101 | })
102 | .then((res) => {
103 | return res.body
104 | })
105 | }
106 |
107 | addComment (comment) {
108 | let self = this
109 | return got.post(this._getBaseUrl() + '/issue/' + comment.issueKey + '/comment?expand=renderedBody', {
110 | auth: self._getAuth(),
111 | body: '{ "body": "' + comment.body + '" }',
112 | headers: {
113 | 'Content-Type': 'application/json'
114 | }
115 | })
116 | .then((res) => {
117 | return res.body
118 | })
119 | }
120 |
121 | }
122 |
123 | module.exports = Jira
124 |
--------------------------------------------------------------------------------
/lib/update.js:
--------------------------------------------------------------------------------
1 | const GhReleases = require('electron-gh-releases')
2 |
3 | const update = (currentVersion) => {
4 | let options = {
5 | repo: 'jenslind/minira',
6 | currentVersion: currentVersion
7 | }
8 |
9 | const updater = new GhReleases(options)
10 |
11 | // Check for updates
12 | // `status` returns true if there is a new update available
13 | updater.check((err, status) => {
14 | if (!err && status) {
15 | // Download the update
16 | updater.download()
17 | }
18 | })
19 |
20 | // When an update has been downloaded
21 | updater.on('update-downloaded', (info) => {
22 | // Restart the app and install the update
23 | updater.install()
24 | })
25 | }
26 |
27 | module.exports = update
28 |
--------------------------------------------------------------------------------
/media/Icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/media/Icon.icns
--------------------------------------------------------------------------------
/media/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/media/icon.png
--------------------------------------------------------------------------------
/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/media/logo.png
--------------------------------------------------------------------------------
/media/minira.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenslind/minira/544392ec3c324962d0f7486e7bf43639bf4079c7/media/minira.png
--------------------------------------------------------------------------------
/menu.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const app = require('electron').app
3 |
4 | const menuTpl = [
5 | {
6 | label: 'Minira',
7 | submenu: [
8 | {
9 | label: 'Quit',
10 | accelerator: 'Command+Q',
11 | click: function () {
12 | app.quit()
13 | }
14 | }
15 | ]
16 | },
17 | {
18 | label: 'Edit',
19 | submenu: [
20 | {
21 | label: 'Undo',
22 | accelerator: 'CmdOrCtrl+Z',
23 | role: 'undo'
24 | },
25 | {
26 | label: 'Redo',
27 | accelerator: 'Shift+CmdOrCtrl+Z',
28 | role: 'redo'
29 | },
30 | {
31 | type: 'separator'
32 | },
33 | {
34 | label: 'Cut',
35 | accelerator: 'CmdOrCtrl+X',
36 | role: 'cut'
37 | },
38 | {
39 | label: 'Copy',
40 | accelerator: 'CmdOrCtrl+C',
41 | role: 'copy'
42 | },
43 | {
44 | label: 'Paste',
45 | accelerator: 'CmdOrCtrl+V',
46 | role: 'paste'
47 | },
48 | {
49 | label: 'Select All',
50 | accelerator: 'CmdOrCtrl+A',
51 | role: 'selectall'
52 | }
53 | ]
54 | }
55 | ]
56 |
57 | module.exports = menuTpl
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minira",
3 | "version": "1.0.1",
4 | "description": "JIRA in your menubar.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "standard",
8 | "postinstall": "electron-rebuild",
9 | "build": "npm test && webpack --config webpack-production.config.js && electron-packager ./ Minira --platform=darwin --arch=x64 --version=0.37.2 --app-version=$npm_package_version --out=./build --overwrite --ignore='^/media$' --ignore='^/build$' --ignore='^/src$' --ignore='^/node_modules/angular2$' --prune --icon=media/Icon.icns --sign='Jens Lind' --app-bundle-id=com.jenslind.minira",
10 | "release": "npm run build && electron-release --app=build/Minira-darwin-x64/Minira.app --output=build/Minira-darwin-x64/Minira-osx-$npm_package_version.zip --token=$MINIRA_TOKEN"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/jenslind/minira.git"
15 | },
16 | "author": "Jens Lind",
17 | "license": "MIT",
18 | "dependencies": {
19 | "angular2": "2.0.0-beta.9",
20 | "electron-gh-releases": "^2.0.2",
21 | "electron-json-storage": "^2.0.0",
22 | "got": "^6.2.0",
23 | "keytar": "^3.0.0",
24 | "menubar": "^4.0.2",
25 | "normalize.css": "^3.0.3",
26 | "zone.js": "0.5.15",
27 | "rxjs": "5.0.0-beta.2",
28 | "reflect-metadata": "0.1.2"
29 | },
30 | "devDependencies": {
31 | "babel-core": "^6.7.0",
32 | "babel-eslint": "^5.0.0",
33 | "babel-loader": "^6.2.4",
34 | "babel-plugin-angular2-annotations": "^5.0.0",
35 | "babel-plugin-transform-class-properties": "^6.6.0",
36 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
37 | "babel-plugin-transform-flow-strip-types": "^6.7.0",
38 | "babel-preset-es2015": "^6.6.0",
39 | "css-loader": "^0.23.1",
40 | "electron-packager": "^5.2.1",
41 | "electron-prebuilt": "0.37.2",
42 | "electron-rebuild": "^1.1.3",
43 | "electron-release": "^2.2.0",
44 | "extract-text-webpack-plugin": "^1.0.1",
45 | "html-loader": "^0.4.3",
46 | "node-sass": "^3.4.2",
47 | "sass-loader": "^3.1.2",
48 | "standard": "^6.0.8",
49 | "style-loader": "^0.13.0",
50 | "webpack": "^1.12.14"
51 | },
52 | "standard": {
53 | "parser": "babel-eslint"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | > Menubar app for [JIRA](https://www.atlassian.com/software/jira)
6 |
7 | [](https://travis-ci.org/jenslind/minira)
8 |
9 | Minira is built on top of [Electron](http://electron.atom.io/), [Menubar](https://github.com/maxogden/menubar) and [Angular 2](https://angular.io/).
10 | It is not a replacement for JIRA:s web interface, the goal is to list a number of important issues for the moment and have easy access to edit them.
11 |
12 | 
13 |
14 | ## The name?
15 |
16 | Jira is coming from the Japanese word for `Godzilla`, `Gojira`. `Minira` is the Japanese word for [Minilla](https://en.wikipedia.org/wiki/Minilla), Godzillas adopted son. :fireworks:
17 |
18 | ## Development
19 |
20 | [](https://github.com/feross/standard)
21 |
22 | Clone this repo and:
23 |
24 | ```
25 | npm install
26 | ```
27 |
28 | Build:
29 | ```
30 | webpack
31 | ```
32 |
33 | Build the .app:
34 | ```
35 | npm run build
36 | ```
37 |
38 | ## License
39 |
40 | MIT © [Jens Lind](http://jenslind.com)
41 |
--------------------------------------------------------------------------------
/src/client/app.injector.js:
--------------------------------------------------------------------------------
1 | import { Injector } from 'angular2/core'
2 |
3 | let appInjectorRef: Injector
4 | export const appInjector = (injector) => {
5 | if (injector) appInjectorRef = injector
6 | return appInjectorRef
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/app.js:
--------------------------------------------------------------------------------
1 | import 'angular2/bundles/angular2-polyfills'
2 | import { appInjector } from './app.injector'
3 | import { Component, View, provide, ComponentRef } from 'angular2/core'
4 | import { RouteConfig, ROUTER_PROVIDERS, LocationStrategy, HashLocationStrategy, RouterLink } from 'angular2/router'
5 | import { AuthRouterOutlet } from './directives/authRouterOutlet.directive'
6 | import { bootstrap } from 'angular2/platform/browser'
7 | import { appRoutes } from './app.routes'
8 | import JiraService from './services/jira.service'
9 | import NavService from './services/nav.service'
10 | import './scss/base/_base'
11 | import './scss/layout/_frame'
12 | import './scss/elements/_logo'
13 | import './scss/components/_btn'
14 | import './scss/components/_labels'
15 | import './scss/components/_loading'
16 |
17 | @Component({
18 | selector: 'jira-app'
19 | })
20 | @View({
21 | directives: [AuthRouterOutlet, RouterLink],
22 | template: require('./templates/app.template.html')
23 | })
24 | @RouteConfig(appRoutes)
25 | class JiraApp {
26 | constructor (nav: NavService) {
27 | this.nav = nav
28 | this.buttons = this.nav.get()
29 | }
30 | }
31 |
32 | document.addEventListener('DOMContentLoaded', () => {
33 | bootstrap(JiraApp, [
34 | ROUTER_PROVIDERS,
35 | provide(LocationStrategy, { useClass: HashLocationStrategy }),
36 | JiraService,
37 | NavService
38 | ])
39 | .then((appRef: ComponentRef) => {
40 | appInjector(appRef.injector)
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/client/app.routes.js:
--------------------------------------------------------------------------------
1 | import IssuesComponent from './components/issues.component'
2 | import IssueComponent from './components/issue.component'
3 | import AuthComponent from './components/auth.component'
4 | import SettingsComponent from './components/settings.component'
5 |
6 | export const appRoutes = [
7 | { path: '/', component: IssuesComponent, as: 'IssuesComponent' },
8 | { path: '/issue/:issueId', component: IssueComponent, as: 'IssueComponent' },
9 | { path: '/auth', component: AuthComponent, as: 'AuthComponent' },
10 | { path: '/settings', component: SettingsComponent, as: 'SettingsComponent' }
11 | ]
12 |
--------------------------------------------------------------------------------
/src/client/components/auth.component.js:
--------------------------------------------------------------------------------
1 | import { Component, View } from 'angular2/core'
2 | import { FormBuilder, Validators } from 'angular2/common'
3 | import { Router } from 'angular2/router'
4 | import JiraService from '../services/jira.service'
5 | import NavService from '../services/nav.service'
6 | import { ipcRenderer } from 'electron'
7 | import '../scss/modules/_auth'
8 |
9 | @Component({
10 | selector: 'auth'
11 | })
12 | @View({
13 | template: require('../templates/auth.template')
14 | })
15 | export default class AuthComponent {
16 | constructor (jira: JiraService, fb: FormBuilder, router: Router, nav: NavService) {
17 | this.fb = fb
18 | this.router = router
19 | this.authForm = this.fb.group({
20 | baseUrl: ['', Validators.required],
21 | user: ['', Validators.required],
22 | pass: ['', Validators.required]
23 | })
24 |
25 | this.hideLoading = true
26 |
27 | nav.hide('settings').hide('issues').hide('shadow')
28 | }
29 |
30 | auth () {
31 | this.hideLoading = false
32 |
33 | let self = this
34 | setTimeout(() => {
35 | const success = ipcRenderer.sendSync('auth', this.authForm.value)
36 | if (success) {
37 | self.router.navigateByUrl('/')
38 | } else {
39 | self.hideLoading = true
40 | }
41 | }, 500)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/client/components/issue.component.js:
--------------------------------------------------------------------------------
1 | import { Component, View, ComponentInstruction } from 'angular2/core'
2 | import { FormBuilder, Validators } from 'angular2/common'
3 | import { RouteParams, CanActivate } from 'angular2/router'
4 | import { appInjector } from '../app.injector'
5 | import JiraService from '../services/jira.service'
6 | import NavService from '../services/nav.service'
7 | import Suggest from './suggest.component'
8 | import '../scss/modules/_issue'
9 | import '../scss/modules/_issue-comments'
10 | import { shell } from 'electron'
11 |
12 | @Component({
13 | selector: 'issue'
14 | })
15 | @View({
16 | directives: [Suggest],
17 | template: require('../templates/issue.template')
18 | })
19 | @CanActivate((next: ComponentInstruction) => {
20 | let jira = appInjector().get(JiraService)
21 | jira.getIssue(next.params.issueId)
22 |
23 | return new Promise((resolve, reject) => {
24 | jira.issue$.subscribe((issue) => {
25 | jira.currentIssue = issue
26 | resolve(true)
27 | })
28 | })
29 | })
30 | export default class IssueComponent {
31 | constructor (jira: JiraService, routeParams: RouteParams, fb: FormBuilder, nav: NavService) {
32 | this.jira = jira
33 | this.params = routeParams.params
34 | this.issue = this.jira.currentIssue
35 | this.assignable = []
36 | this.fb = fb
37 | this.hideTransitions = true
38 | this.hideComments = true
39 | this.addingComment = false
40 | this.comments = null
41 |
42 | nav.show('settings').show('issues').show('shadow')
43 | }
44 |
45 | getAssignable () {
46 | const users = []
47 | for (let i = 0, len = this.assignable.length; i < len; i++) {
48 | users.push(this.assignable[i].name)
49 | }
50 | return users
51 | }
52 |
53 | assignUser () {
54 | if (!this.assignForm.valid) return
55 | const user = this.assignForm.controls.assigned.value.toLowerCase()
56 | if (user === this.issue.fields.assignee.name) return
57 | this.jira.assignUser(this.issue.self, user)
58 | }
59 |
60 | getPossibleTransitions () {
61 | let transitions = []
62 | let len = this.issue.transitions.length
63 | for (let i = 0; i < len; i++) {
64 | if (this.issue.transitions[i].to.id !== this.issue.fields.status.id) {
65 | transitions.push(this.issue.transitions[i])
66 | }
67 | }
68 | return transitions
69 | }
70 |
71 | doTransition (iId, t) {
72 | this.jira.doTransition(iId, t.id)
73 | this.issue.fields.status = t.to
74 | this.transitions = this.getPossibleTransitions()
75 | }
76 |
77 | openIssue () {
78 | const URLParts = this.issue.self.split('/')
79 | let issueURL = URLParts[0] + '//' + URLParts[2]
80 | issueURL += '/projects/' + this.issue.fields.project.key
81 | issueURL += '/issues/' + this.issue.key
82 | shell.openExternal(issueURL)
83 | }
84 |
85 | showComments () {
86 | this.jira.getComments(this.issue.key)
87 |
88 | let self = this
89 | this.comments = null
90 | this.jira.comments$.subscribe((comments) => {
91 | if (comments.comments) {
92 | self.comments = comments.comments
93 | } else {
94 | self.comments.push(JSON.parse(comments))
95 | self.addingComment = false
96 | setTimeout(() => { document.querySelector('.issue__comments .issue__comment:last-child').scrollIntoView() }, 100)
97 | }
98 | self.hideComments = false
99 | })
100 | }
101 |
102 | addComment () {
103 | this.addingComment = true
104 | let comment = this.commentForm.value
105 | comment.issueKey = this.issue.key
106 | comment.body = comment.body.replace(/\n/g, '\\n')
107 | this.jira.addComment(comment)
108 | this.commentForm.controls.body.updateValue('')
109 | }
110 |
111 | ngOnInit () {
112 | let self = this
113 | this.jira.getAssignable(this.issue.key, (data) => {
114 | self.assignable = data
115 | })
116 |
117 | this.transitions = this.getPossibleTransitions()
118 |
119 | let assignee = (this.issue.fields.assignee) ? this.issue.fields.assignee.name : ''
120 | this.assignForm = this.fb.group({
121 | assigned: [assignee, Validators.required]
122 | })
123 |
124 | this.commentForm = this.fb.group({
125 | body: ['', Validators.required]
126 | })
127 | }
128 |
129 | _strAsDate (str) {
130 | return new Date(str)
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/client/components/issues.component.js:
--------------------------------------------------------------------------------
1 | import { Component, View } from 'angular2/core'
2 | import { ROUTER_DIRECTIVES } from 'angular2/router'
3 | import JiraService from '../services/jira.service'
4 | import NavService from '../services/nav.service'
5 | import { ipcRenderer } from 'electron'
6 | import '../scss/modules/_issues'
7 |
8 | @Component({
9 | selector: 'issues'
10 | })
11 | @View({
12 | directives: [ROUTER_DIRECTIVES],
13 | template: require('../templates/issues.template')
14 | })
15 | export default class IssuesComponent {
16 | constructor (jira: JiraService, nav: NavService) {
17 | this.jira = jira
18 | this.issues = []
19 | this.hideZero = true
20 | this.hideLoading = false
21 |
22 | nav.show('settings').hide('issues').show('shadow')
23 | }
24 |
25 | fillLoad (event) {
26 | this.loadingPosition = 'top: ' + (event.clientY - 50) + 'px; left: ' + event.clientX + 'px;'
27 | this.hideLoading = false
28 | }
29 |
30 | ngOnInit () {
31 | let self = this
32 | this.jira.issues$.subscribe((issues) => {
33 | self.issues = issues
34 | self.hideLoading = true
35 | if (!issues.length) self.hideZero = false
36 | })
37 | this.settings = ipcRenderer.sendSync('getSettings')
38 | this.jira.getIssues(this.settings.issueQuery)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/client/components/settings.component.js:
--------------------------------------------------------------------------------
1 | import { Component, View } from 'angular2/core'
2 | import { FormBuilder } from 'angular2/common'
3 | import { Router } from 'angular2/router'
4 | import NavService from '../services/nav.service'
5 | import { ipcRenderer } from 'electron'
6 | import '../scss/modules/_settings'
7 |
8 | @Component({
9 | selector: 'settings'
10 | })
11 | @View({
12 | template: require('../templates/settings.template')
13 | })
14 | export default class SettingsComponent {
15 | constructor (fb: FormBuilder, nav: NavService, router: Router) {
16 | this.fb = fb
17 | this.router = router
18 | this.user = ipcRenderer.sendSync('isAuthed')
19 | this.settings = ipcRenderer.sendSync('getSettings')
20 |
21 | this.settingsForm = this.fb.group({
22 | issueQuery: [this.settings.issueQuery]
23 | })
24 |
25 | nav.show('settings').show('issues').show('shadow')
26 | }
27 |
28 | unlink () {
29 | let done = ipcRenderer.sendSync('unAuth')
30 | if (done) this.router.navigateByUrl('/auth')
31 | }
32 |
33 | updateSettings () {
34 | ipcRenderer.send('updateSettings', this.settingsForm.value)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/client/components/suggest.component.js:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectorRef } from 'angular2/core'
2 | import '../scss/modules/_suggest'
3 |
4 | @Component({
5 | selector: 'suggest',
6 | inputs: ['haystack', 'control', 'value'],
7 | template: `
8 | `
9 | })
10 | export default class Suggest {
11 | constructor (cdr: ChangeDetectorRef) {
12 | this.cdr = cdr
13 | this.suggest = ''
14 | }
15 |
16 | ngOnInit () {
17 | this.suggest = this.control.value
18 | this.currentValue = this.control.value
19 | }
20 |
21 | getSuggestion (event) {
22 | const enterKey = 13
23 |
24 | let regex = new RegExp('^' + event.target.value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), 'i')
25 | this.suggestFound = false
26 | for (let i = 0, len = this.haystack.length; i < len; i++) {
27 | if (regex.test(this.haystack[i]) && event.target.value) {
28 | this.suggestFound = this.haystack[i]
29 | }
30 | }
31 |
32 | if (this.suggestFound) {
33 | this.suggest = this.suggestFound
34 | } else {
35 | this.suggest = null
36 | }
37 |
38 | if (event.which === enterKey) {
39 | event.target.value = this.suggest || this.currentValue
40 | this.currentValue = this.suggest || this.currentValue
41 | }
42 |
43 | this.cdr.detectChanges()
44 | }
45 |
46 | fixValue (event) {
47 | event.target.value = this.currentValue
48 | this.suggest = this.currentValue
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/client/directives/authRouterOutlet.directive.js:
--------------------------------------------------------------------------------
1 | import { Directive, AttributeMetadata, ElementRef, DynamicComponentLoader } from 'angular2/core'
2 | import { Router, RouterOutlet, ComponentInstruction } from 'angular2/router'
3 | import JiraService from '../services/jira.service'
4 |
5 | @Directive({
6 | selector: 'router-outlet'
7 | })
8 | @Reflect.metadata('parameters', [[new AttributeMetadata('name')]])
9 | export class AuthRouterOutlet extends RouterOutlet {
10 | constructor (nameAttr, _elementRef: ElementRef, _loader: DynamicComponentLoader, _parentRouter: Router, jira: JiraService) {
11 | super(_elementRef, _loader, _parentRouter, nameAttr)
12 |
13 | this.parentRouter = _parentRouter
14 | this.jira = jira
15 | }
16 |
17 | activate (instruction: ComponentInstruction) {
18 | if (!this.parentRouter.isRouteActive(this.parentRouter.generate(['/AuthComponent'])) &&
19 | !this.jira.isAuthed()) {
20 | this.parentRouter.navigateByUrl('/auth')
21 | }
22 |
23 | return super.activate(instruction)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/client/scss/base/_base.scss:
--------------------------------------------------------------------------------
1 | * {
2 | -webkit-font-smoothing: antialiased;
3 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
4 | box-sizing: border-box;
5 | outline: 0;
6 | }
7 |
8 | html {
9 | font-size: 62.5%;
10 | min-height: 100%;
11 | font-family: 'Montserrat', sans-serif;
12 | overflow: hidden;
13 | }
14 |
15 | input,
16 | form,
17 | select,
18 | button {
19 | -webkit-font-smoothing: antialiased;
20 | }
21 |
22 | button,
23 | [type=submit] {
24 | border: 0;
25 | }
26 |
27 | [hidden] {
28 | display: none;
29 | }
30 |
--------------------------------------------------------------------------------
/src/client/scss/components/_btn.scss:
--------------------------------------------------------------------------------
1 | .frame__header__button_wrapper {
2 | width: 100px;
3 |
4 | &--right {
5 | text-align: right;
6 | }
7 | }
8 |
9 | .frame__header__button {
10 | border: 1px solid #614385;
11 | border-radius: 3px;
12 | color: #614385;
13 | background: transparent;
14 | transition: all .1s;
15 | text-transform: uppercase;
16 | outline: 0;
17 |
18 | &:hover {
19 | opacity: .4;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/client/scss/components/_labels.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .label {
4 | background: #CCCCCC;
5 | color: #333333;
6 | border-radius: 2px;
7 | text-transform: uppercase;
8 | font-size: 1rem;
9 |
10 | &.blue-gray {
11 | background: #4A6785;
12 | color: $color-white;
13 | }
14 |
15 | &.yellow {
16 | background: #FFD351;
17 | color: #594300;
18 | }
19 |
20 | &.medium-gray {
21 | background: #CCCCCC;
22 | color: #333333;
23 | }
24 |
25 | &.green {
26 | background: #14892c;
27 | color: $color-white;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/client/scss/components/_loading.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | /**
4 | * Thanks George Hastings <3
5 | * http://codepen.io/georgehastings/pen/QyXeNR
6 | */
7 | .loading {
8 | position: relative;
9 | background-color: #F8E71C;
10 | width: 10px;
11 | height: 10px;
12 | margin: 0 auto;
13 | border-radius: 50%;
14 |
15 | &--auth {
16 | margin: 50px 0;
17 | }
18 |
19 | &--issues {
20 | margin: 140px auto;
21 | animation: fadeIn .5s;
22 | animation-delay: .2s;
23 | animation-fill-mode: forwards;
24 | position: fixed;
25 | left: 50%;
26 | margin-left: -8px;
27 | z-index: 10;
28 | opacity: 0;
29 | }
30 |
31 | &:after, &:before {
32 | content: "";
33 | position: absolute;
34 | width: 5px;
35 | height: 5px;
36 | border-radius: 50%;
37 | }
38 |
39 | &:after {
40 | left: -10px;
41 | top: -5px;
42 | background-color: hotpink;
43 | transform-origin: 15px 10px;
44 | animation: axis 1s linear infinite;
45 | }
46 |
47 | &:before {
48 | left: -25px;
49 | top: -15px;
50 | background-color: lightblue;
51 | transform-origin: 30px 20px;
52 | animation: axis 2s linear infinite;
53 | }
54 | }
55 |
56 | @keyframes axis {
57 | 0% {
58 | transform: rotateZ(0deg) translate3d(0,0,0);
59 | }
60 | 100% {
61 | transform: rotateZ(360deg) translate3d(0,0,0);
62 | }
63 | }
64 |
65 | @keyframes fadeIn {
66 | 0% {
67 | opacity: 0;
68 | }
69 | 100% {
70 | opacity: 1;
71 | }
72 | }
73 |
74 | // http://codepen.io/mrrocks/pen/EiplA <3
75 |
76 | .loading__material {
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | right: 0;
81 | bottom: 0;
82 |
83 | &--add-comment {
84 | background-color: darken($color-bg-blue, 10%);
85 | z-index: 5;
86 |
87 | .spinner {
88 | margin-top: 15px;
89 | }
90 | }
91 |
92 | .spinner {
93 | animation: material-rotator 1.4s linear infinite;
94 | }
95 |
96 | .path {
97 | stroke-dasharray: 187;
98 | stroke-dashoffset: 0;
99 | transform-origin: center;
100 | animation: material-dash 1.4s ease-in-out infinite;
101 | stroke: $color-bg-blue-text;
102 | }
103 | }
104 |
105 | @keyframes material-rotator {
106 | 0% {
107 | transform: rotate(0deg);
108 | }
109 |
110 | 100% {
111 | transform: rotate(270deg);
112 | }
113 | }
114 |
115 | @keyframes material-dash {
116 | 0% {
117 | stroke-dashoffset: 187;
118 | }
119 |
120 | 50% {
121 | stroke-dashoffset: (187/4);
122 | transform:rotate(135deg);
123 | }
124 |
125 | 100% {
126 | stroke-dashoffset: 187;
127 | transform:rotate(450deg);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/client/scss/elements/_logo.scss:
--------------------------------------------------------------------------------
1 | .logo {
2 | font-family: 'Playfair Display', serif;
3 | font-size: 1.4rem;
4 | background: linear-gradient(to left, #614385 , #516395);
5 | -webkit-background-clip: text;
6 | -webkit-text-fill-color: transparent;
7 | font-weight: 700;
8 | }
9 |
--------------------------------------------------------------------------------
/src/client/scss/layout/_frame.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .frame {
4 | width: 100%;
5 | height: 385px;
6 | position: absolute;
7 | bottom: 0;
8 | }
9 |
10 | .frame__header {
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 | width: 100%;
15 | height: 40px;
16 | background: $color-white;
17 | border-top-left-radius: 5px;
18 | border-top-right-radius: 5px;
19 | position: relative;
20 | z-index: 6;
21 | padding-left: 10px;
22 | padding-right: 10px;
23 | box-shadow: 0 2px 3px darken($color-bg-blue, 10%);
24 |
25 | &--no-shadow {
26 | box-shadow: none;
27 | }
28 |
29 | &:before {
30 | bottom: 100%;
31 | left: 50%;
32 | border: solid transparent;
33 | content: "";
34 | height: 0;
35 | width: 0;
36 | position: absolute;
37 | pointer-events: none;
38 | border-bottom-color: #aaa;
39 | border-width: 11px;
40 | margin-left: -11px;
41 | }
42 |
43 | &:after {
44 | bottom: 100%;
45 | left: 50%;
46 | border: solid transparent;
47 | content: "";
48 | height: 0;
49 | width: 0;
50 | position: absolute;
51 | pointer-events: none;
52 | border-bottom-color: $color-white;
53 | border-width: 10px;
54 | margin-left: -10px;
55 | }
56 | }
57 |
58 | .frame__content {
59 | overflow-y: auto;
60 | height: calc(100% - 40px);
61 | background: $color-bg-blue;
62 | color: $color-bg-blue-text;
63 | }
64 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_auth.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .auth {
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | width: 100%;
8 | height: 100%;
9 | background: url('./graphics/auth-bg.png');
10 | position: relative;
11 | z-index: 1;
12 | border-top: 1px solid darken(#003463, 15%);
13 | box-shadow: inset 0 3px 5px darken(#003463, 5%);
14 |
15 | h4 {
16 | font-size: 1.2rem;
17 | color: $color-white;
18 | margin-top: 0;
19 | margin-bottom: 30px;
20 | }
21 | }
22 |
23 | .auth__form {
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: center;
27 | align-items: center;
28 | width: 100%;
29 |
30 | input[type=text],
31 | input[type=password] {
32 | width: 60%;
33 | border: 2px solid $color-white;
34 | border-radius: 2px;
35 | color: $color-white;
36 | padding: 10px;
37 | font-size: 1.4rem;
38 | background: transparent;
39 | margin-bottom: 25px;
40 | outline: 0;
41 |
42 | &::-webkit-input-placeholder {
43 | color: $color-white;
44 | }
45 |
46 | &.auth__form__no-margin {
47 | margin-bottom: 0;
48 | }
49 | }
50 | }
51 |
52 | .auth__submit {
53 | width: 170px;
54 | height: 35px;
55 | border-radius: 30px;
56 | color: #fff;
57 | text-transform: uppercase;
58 | background: linear-gradient(to bottom, #80d44e, #47ae0e);
59 | font-size: 1.4rem;
60 | outline: 0;
61 |
62 | &:hover {
63 | background: linear-gradient(to bottom, lighten(#80d44e, 10%), lighten(#47ae0e, 10%));
64 | }
65 | }
66 |
67 | .auth__loading {
68 | display: flex;
69 | flex-direction: column;
70 | align-items: center;
71 | justify-content: center;
72 | position: absolute;
73 | background: rgba(#000, .75);
74 | top: 0;
75 | left: 0;
76 | right: 0;
77 | bottom: 0;
78 |
79 | h5 {
80 | font-size: 1.2rem;
81 | text-transform: uppercase;
82 | color: $color-white;
83 | }
84 | }
85 |
86 | .auth__form__description {
87 | color: $color-white;
88 | margin: 10px 0 15px;
89 | font-style: italic;
90 | width: 60%;
91 | }
92 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_issue-comments.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .issue__comments__wrapper {
4 | background: rgba($color-bg-blue, .95);
5 | position: absolute;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | top: 40px;
10 | padding: 15px;
11 | padding-bottom: 75px;
12 | z-index: 3;
13 | font-size: 1.4rem;
14 | opacity: 0;
15 | display: none;
16 |
17 | &.active {
18 | display: block;
19 | animation: fadeZoomIn .5s;
20 | animation-fill-mode: forwards;
21 | }
22 |
23 | h2 {
24 | margin-top: 0;
25 | }
26 | }
27 |
28 | .issue__comments {
29 | overflow-y: scroll;
30 | position: absolute;
31 | left: 0;
32 | right: 0;
33 | bottom: 60px;
34 | top: 40px;
35 | padding: 15px;
36 | }
37 |
38 | .issue__comments__show {
39 | width: 35px;
40 | height: 24px;
41 | border-radius: 100%;
42 | background: url('./graphics/comments.svg') center center no-repeat;
43 | margin: 11px 5px 0;
44 | transition: transform .2s;
45 |
46 | &:hover {
47 | transform: scale(1.1);
48 | }
49 | }
50 |
51 | .issue__comments__hide {
52 | position: fixed;
53 | right: 10px;
54 | top: 14px;
55 | height: 25px;
56 | border-radius: 3px;
57 | background: darken($color-bg-blue, 10%);
58 | z-index: 5;
59 | }
60 |
61 | .issue__comments__add {
62 | position: fixed;
63 | bottom: 0;
64 | left: 0;
65 | right: 0;
66 | height: 60px;
67 |
68 | textarea {
69 | width: 80%;
70 | padding: 5px;
71 | float: left;
72 | border: 0;
73 | border-top: 1px solid darken($color-bg-blue, 10%);
74 | }
75 |
76 | button[type="submit"] {
77 | width: 20%;
78 | height: 60px;
79 | background-color: darken($color-bg-blue, 10%);
80 | color: $color-bg-blue-text;
81 | transition: background-color .2s;
82 | position: relative;
83 |
84 | &:hover {
85 | background-color: darken($color-bg-blue, 15%);
86 | }
87 |
88 | &.adding {
89 | background-image: url('./graphics/spinn-loader.svg');
90 | background-position: center;
91 | background-repeat: no-repeat;
92 | font-size: 0;
93 | }
94 | }
95 | }
96 |
97 | .issue__comment {
98 | margin-bottom: 15px;
99 | }
100 |
101 | .issue__comment__name {
102 | text-transform: uppercase;
103 | font-size: 1.1rem;
104 | margin-bottom: 5px;
105 | }
106 |
107 | .issue__comment__body {
108 | background: $color-white;
109 | border-radius: 5px;
110 | padding: 10px;
111 | box-shadow: 0 1px 3px darken($color-bg-blue, 10%);
112 |
113 | p {
114 | margin: 0;
115 | margin-bottom: 10px;
116 | }
117 | }
118 |
119 | @keyframes fadeZoomIn {
120 | 0% {
121 | opacity: 0;
122 | transform: scale(0);
123 | }
124 | 100% {
125 | opacity: 1;
126 | transform: scale(1);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_issue.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .issue {
4 | padding-bottom: 45px;
5 | }
6 |
7 | .issue__header {
8 | border-bottom: 1px solid darken($color-bg-blue, 10%);
9 | color: $color-bg-blue-text;
10 | height: 140px;
11 | padding: 0 10px;
12 |
13 | h1 {
14 | font-size: 1.8rem;
15 | margin-left: 10px;
16 | font-weight: normal;
17 |
18 | &:hover {
19 | text-decoration: underline;
20 | cursor: pointer;
21 | }
22 | }
23 | }
24 |
25 | .issue__header__top {
26 | display: flex;
27 | flex-direction: row;
28 | align-items: center;
29 | border-bottom: 2px solid darken($color-bg-blue, 10%);
30 | }
31 |
32 | .issue__header__avatars {
33 | display: flex;
34 | flex-direction: row;
35 | justify-content: center;
36 | align-items: center;
37 | padding: 20px 0;
38 | }
39 |
40 | .issue__header__avatars__avatar {
41 | width: 50%;
42 | font-size: 1.2rem;
43 | text-transform: uppercase;
44 | text-overflow: ellipsis;
45 | white-space: nowrap;
46 | overflow: hidden;
47 | margin: 0 5px;
48 |
49 | span {
50 | color: darken($color-bg-blue-text, 5%);
51 | font-size: 1.1rem;
52 | display: block;
53 | margin-top: 10px;
54 | margin-bottom: 3px;
55 | }
56 |
57 | img {
58 | width: 40px;
59 | height: 40px;
60 | border-radius: 100%;
61 | float: left;
62 | margin-right: 12px;
63 | margin-top: 5px;
64 | }
65 | }
66 |
67 | .issue__content {
68 | background: $color-bg-white;
69 | color: $color-bg-white-text;
70 | font-size: 1.4rem;
71 | padding: 10px;
72 | min-height: 160px;
73 | }
74 |
75 | .issue__footer {
76 | display: flex;
77 | flex-direction: row;
78 | position: absolute;
79 | bottom: 0;
80 | left: 0;
81 | right: 0;
82 | height: 45px;
83 | background: #fff;
84 | border-top: 1px solid darken(#F7F8FB, 15%);
85 |
86 | input[type="submit"] {
87 | display: none;
88 | }
89 |
90 | input[type="text"] {
91 | border: 0;
92 | }
93 |
94 | form:first-child {
95 | margin-right: auto;
96 | }
97 |
98 | form:last-child {
99 | margin-left: auto;
100 | }
101 | }
102 |
103 | .issue__transition {
104 | display: flex;
105 | justify-content: center;
106 | align-items: center;
107 | padding-right: 10px;
108 | }
109 |
110 | .issue__transitions {
111 | background: $color-white;
112 | position: absolute;
113 | bottom: 45px;
114 | right: 0;
115 | left: 0;
116 | padding: 10px;
117 | display: flex;
118 | flex-direction: row;
119 | align-items: flex-start;
120 | flex-wrap: wrap;
121 | border-top: 1px solid darken(#F7F8FB, 15%);
122 |
123 | .label {
124 | margin-right: 10px;
125 | margin-bottom: 5px;
126 | margin-top: 5px;
127 |
128 | &:last-child {
129 | margin-right: 0;
130 | }
131 | }
132 | }
133 |
134 | .issue__priority {
135 | display: flex;
136 | justify-content: center;
137 | align-items: center;
138 | margin-left: auto;
139 | width: 22px;
140 | height: 22px;
141 | background: $color-white;
142 | border-radius: 100%;
143 | border: 1px solid #e98641;
144 |
145 | img {
146 | width: 14px;
147 | height: auto;
148 | }
149 | }
150 |
151 | .issue__footer__label {
152 | position: absolute;
153 | z-index: 2;
154 | top: -10px;
155 | background: darken(#F7F8FB, 15%);
156 | padding: 3px;
157 | font-size: .9rem;
158 | text-transform: uppercase;
159 | left: 8px;
160 | }
161 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_issues.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .issues {
4 | display: flex;
5 | flex-direction: column;
6 | position: relative;
7 |
8 | &--hideOverflow {
9 | overflow: hidden;
10 | }
11 | }
12 |
13 | .issues__item {
14 | display: flex;
15 | flex-direction: row;
16 | align-items: center;
17 | justify-content: space-between;
18 | width: calc(100% - 20px);
19 | height: 60px;
20 | cursor: pointer;
21 | padding-right: 10px;
22 | padding-bottom: 15px;
23 | padding-top: 15px;
24 | background: $color-white;
25 | margin-left: 10px;
26 | margin-right: 10px;
27 | margin-bottom: 10px;
28 | box-shadow: 0 1px 3px darken($color-bg-blue, 10%);
29 | border-radius: 3px;
30 |
31 | &:nth-child(3) {
32 | margin-top: 10px;
33 | }
34 | }
35 |
36 | .issues__item__content {
37 | display: flex;
38 | flex-direction: column;
39 | width: 250px;
40 | }
41 |
42 | .issues__item__title {
43 | display: block;
44 | max-width: 250px;
45 | white-space: nowrap;
46 | overflow: hidden;
47 | text-overflow: ellipsis;
48 | color: darken($color-bg-white-text, 10%);
49 | font-size: 1.4rem;
50 | }
51 |
52 | .issues__item__project {
53 | display: block;
54 | margin-top: 5px;
55 | color: $color-bg-white-text;
56 | text-transform: uppercase;
57 | font-size: 1.2rem;
58 | }
59 |
60 | .issues__item__status {
61 | width: 10px;
62 | height: 10px;
63 | border-radius: 100%;
64 | background: green;
65 | margin-right: 10px;
66 | }
67 |
68 | .issues__item__avatar {
69 | display: flex;
70 | width: 60px;
71 | height: 60px;
72 | align-items: center;
73 | justify-content: center;
74 | position: relative;
75 |
76 | .issues__item__avatar__img {
77 | width: 38px;
78 | height: 38px;
79 | border-radius: 100%;
80 | }
81 | }
82 |
83 | .issues__item__priority {
84 | width: 20px;
85 | height: 20px;
86 | position: absolute;
87 | bottom: 6px;
88 | right: 6px;
89 | background: $color-white;
90 | padding: 5px;
91 | border-radius: 100%;
92 | }
93 |
94 | .issues__item__type {
95 | background: darken($color-bg-blue, 10%);
96 | padding: 5px;
97 | border-radius: 100%;
98 | margin-left: auto;
99 | }
100 |
101 | .issues__zero {
102 | position: relative;
103 | width: 100%;
104 | height: 100%;
105 | display: flex;
106 | flex-direction: column;
107 | align-items: center;
108 | margin-top: 50px;
109 |
110 | &:after {
111 | display: block;
112 | content: '';
113 | position: absolute;
114 | top: 0;
115 | left: 0;
116 | right: 0;
117 | bottom: 0;
118 | background-size: cover;
119 | opacity: .1;
120 | z-index: 1;
121 | }
122 |
123 | &[hidden] {
124 | display: none;
125 | }
126 |
127 | h2 {
128 | color: $color-bg-blue-text;
129 | }
130 | }
131 |
132 | .issues__zero__done {
133 | width: 150px;
134 | height: 150px;
135 | border-radius: 100%;
136 | background: url('./graphics/done.gif');
137 | background-size: cover;
138 | background-position: center;
139 | position: relative;
140 | z-index: 2;
141 | }
142 |
143 | .issues__loading {
144 | position: absolute;
145 | animation: scaleFill .5s;
146 | animation-fill-mode: forwards;
147 | width: 10px;
148 | height: 10px;
149 | background: $color-bg-blue;
150 | z-index: 5;
151 | border-radius: 100%;
152 | }
153 |
154 | @keyframes scaleFill {
155 | from {
156 | transform: scale(1);
157 | }
158 |
159 | to {
160 | transform: scale(100);
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_settings.scss:
--------------------------------------------------------------------------------
1 | @import 'src/client/scss/utils/_colors';
2 |
3 | .settings {
4 | padding: 20px;
5 | height: 100%;
6 |
7 | fieldset {
8 | border: 0;
9 | border-top: 1px solid $color-bg-blue-text;
10 | padding: 0;
11 | padding-top: 15px;
12 | margin-bottom: 15px;
13 | }
14 |
15 | legend {
16 | font-size: 1.4rem;
17 | text-transform: uppercase;
18 | padding: 10px 20px;
19 | background: $color-bg-blue;
20 | margin: 0 auto;
21 | }
22 |
23 | label {
24 | font-size: 1.4rem;
25 | display: block;
26 | margin-bottom: 5px;
27 | }
28 |
29 | input[type="text"] {
30 | display: block;
31 | border: 2px solid $color-bg-blue-text;
32 | border-radius: 2px;
33 | padding: 10px;
34 | font-size: 1.4rem;
35 | color: $color-bg-blue-text;
36 | outline: 0;
37 | width: 100%;
38 | background: transparent;
39 | }
40 | }
41 |
42 | .settings__account {
43 | display: flex;
44 | flex-direction: column;
45 | align-items: center;
46 | justify-content: center;
47 | font-size: 1.4rem;
48 | text-align: center;
49 |
50 | img {
51 | width: 48px;
52 | height: 48px;
53 | border-radius: 100%;
54 | }
55 |
56 | input[type="submit"] {
57 | background: #e39a9a;
58 | color: #833737;
59 | border-radius: 2px;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/client/scss/modules/_suggest.scss:
--------------------------------------------------------------------------------
1 | suggest {
2 | display: block;
3 | width: 200px;
4 | height: 40px;
5 | position: relative;
6 | }
7 |
8 | .suggest__input {
9 | border: 1px solid #000;
10 | background: transparent;
11 | width: 100%;
12 | height: 40px;
13 | padding: 10px;
14 | color: #333;
15 | font-size: 14px;
16 | position: relative;
17 | z-index: 2;
18 | text-transform: lowercase;
19 | }
20 |
21 | .suggest__suggestion {
22 | position: absolute;
23 | width: 100%;
24 | height: 40px;
25 | padding: 10px;
26 | color: #888;
27 | font-size: 14px;
28 | line-height: 1.5;
29 | top: 0;
30 | left: 0;
31 | z-index: 1;
32 | text-transform: lowercase;
33 | }
34 |
--------------------------------------------------------------------------------
/src/client/scss/utils/_colors.scss:
--------------------------------------------------------------------------------
1 | $color-white: #fff;
2 |
3 | $color-bg-blue: #f1f4f7;
4 | $color-bg-blue-text: #697583;
5 |
6 | $color-bg-white: $color-white;
7 | $color-bg-white-text: #8b8b8b;
8 |
--------------------------------------------------------------------------------
/src/client/services/jira.service.js:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs/Subject'
2 | import { ipcRenderer } from 'electron'
3 | import { NgZone } from 'angular2/core'
4 |
5 | export default class JiraService {
6 | constructor (zone: NgZone) {
7 | this.issues = new Subject()
8 | this.issue = new Subject()
9 | this.comments = new Subject()
10 | this.issues$ = this.issues.asObservable()
11 | this.issue$ = this.issue.asObservable()
12 | this.comments$ = this.comments.asObservable()
13 | this.zone = zone
14 |
15 | this.onIssues()
16 | this.onIssue()
17 | this.onComments()
18 | }
19 |
20 | getIssues (jql) {
21 | ipcRenderer.send('getIssues', jql)
22 | }
23 |
24 | onIssues () {
25 | let self = this
26 | ipcRenderer.on('issues', (event, issues) => {
27 | self.zone.run(() => {
28 | self.issues.next(issues)
29 | })
30 | })
31 | }
32 |
33 | getIssue (id) {
34 | ipcRenderer.send('getIssue', id)
35 | }
36 |
37 | onIssue () {
38 | let self = this
39 | ipcRenderer.on('issue', (event, issue) => {
40 | self.zone.run(() => {
41 | self.issue.next(issue)
42 | })
43 | })
44 | }
45 |
46 | getComments (issueId) {
47 | ipcRenderer.send('getComments', issueId)
48 | }
49 |
50 | onComments () {
51 | let self = this
52 | ipcRenderer.on('comments', (event, comments) => {
53 | self.zone.run(() => {
54 | self.comments.next(comments)
55 | })
56 | })
57 |
58 | ipcRenderer.on('commentAdded', (event, newComment) => {
59 | self.zone.run(() => {
60 | self.comments.next(newComment)
61 | })
62 | })
63 | }
64 |
65 | addComment (comment) {
66 | ipcRenderer.send('addComment', comment)
67 | }
68 |
69 | getAssignable (issueId, cb) {
70 | let self = this
71 | ipcRenderer.send('getAssignable', issueId)
72 | ipcRenderer.on('assignable', (event, data) => {
73 | self.zone.run(() => {
74 | cb(data)
75 | })
76 | })
77 | }
78 |
79 | assignUser (issue, user) {
80 | ipcRenderer.send('assignUser', {issue: issue, user: user})
81 | }
82 |
83 | getTransitions (issueId) {
84 | return ipcRenderer.sendSync('getTransitions', issueId)
85 | }
86 |
87 | doTransition (issueId, transitionId) {
88 | ipcRenderer.send('doTransition', {issueId, transitionId})
89 | }
90 |
91 | isAuthed () {
92 | return ipcRenderer.sendSync('isAuthed')
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/client/services/nav.service.js:
--------------------------------------------------------------------------------
1 | export default class NavService {
2 | constructor () {
3 | this.button = {}
4 | this.button.settings = true
5 | this.button.issues = true
6 | this.button.shadow = true
7 | }
8 |
9 | show (btn) {
10 | this.button[btn] = false
11 | return this
12 | }
13 |
14 | hide (btn) {
15 | this.button[btn] = true
16 | return this
17 | }
18 |
19 | get () {
20 | return this.button
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/client/templates/app.template.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/client/templates/auth.template.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
Trying to auth..
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/client/templates/issue.template.html:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
25 |
43 |
44 |
67 |
68 |
--------------------------------------------------------------------------------
/src/client/templates/issues.template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |

8 |
9 |
10 | {{issue.fields.summary}}
11 | {{issue.fields.project.name}}
12 |
13 |

14 |
15 |
16 |
17 |
No issues found
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/client/templates/settings.template.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/webpack-production.config.js:
--------------------------------------------------------------------------------
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | entry: {
6 | app: ['./src/client/app.js']
7 | },
8 | module: {
9 | loaders: [
10 | { test: /\.js$/,
11 | exclude: /node_modules/,
12 | loader: 'babel-loader'
13 | },
14 | {
15 | test: /\.scss$/,
16 | exclude: /node_modules/,
17 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader?-url&minimize!sass-loader')
18 | },
19 | {
20 | test: /\.html$/,
21 | exclude: /node_modules/,
22 | loader: 'html-loader?attrs=false'
23 | }
24 | ]
25 | },
26 | output: {
27 | filename: 'bundle.js',
28 | path: './'
29 | },
30 | plugins: [
31 | new ExtractTextPlugin('bundle.css'),
32 | new webpack.IgnorePlugin(/vertx/),
33 | new webpack.optimize.UglifyJsPlugin({
34 | mangle: false
35 | })
36 | ],
37 | resolve: {
38 | extensions: ['', '.scss', '.js', '.html'],
39 | modulesDirectories: ['src', 'node_modules']
40 | },
41 | target: 'electron',
42 | htmlLoader: {
43 | minimize: true,
44 | removeAttributeQuotes: false,
45 | caseSensitive: true,
46 | customAttrSurround: [[/#/, /(?:)/], [/\*/, /(?:)/], [/\[?\(?/, /(?:)/]],
47 | customAttrAssign: [/\)?\]?=/]
48 | }
49 | }
50 |
51 | module.exports = config
52 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | entry: {
6 | app: ['./src/client/app.js']
7 | },
8 | module: {
9 | loaders: [
10 | { test: /\.js$/,
11 | exclude: /node_modules/,
12 | loader: 'babel-loader'
13 | },
14 | {
15 | test: /\.scss$/,
16 | exclude: /node_modules/,
17 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader?-url&sourceMap!sass-loader')
18 | },
19 | {
20 | test: /\.html$/,
21 | exclude: /node_modules/,
22 | loader: 'html-loader?attrs=false'
23 | }
24 | ]
25 | },
26 | output: {
27 | filename: 'bundle.js',
28 | path: './'
29 | },
30 | plugins: [
31 | new ExtractTextPlugin('bundle.css'),
32 | new webpack.IgnorePlugin(/vertx/)
33 | ],
34 | resolve: {
35 | extensions: ['', '.scss', '.js', '.html'],
36 | modulesDirectories: ['src', 'node_modules']
37 | },
38 | devtool: '#inline-source-map',
39 | target: 'electron'
40 | }
41 |
42 | module.exports = config
43 |
--------------------------------------------------------------------------------
Comments
46 | 47 | 48 |