diff --git a/src/css/_form.scss b/src/css/_form.scss
index 497f510..8d470dc 100644
--- a/src/css/_form.scss
+++ b/src/css/_form.scss
@@ -35,7 +35,7 @@
}
.input-group-addon {
- padding: 0.5rem 0.75rem;
+ padding: 0.25rem 0.5rem;
font-weight: normal;
color: #555555;
text-align: center;
diff --git a/src/images/configurationSettings.png b/src/images/configurationSettings.png
new file mode 100644
index 0000000..ebdf99d
Binary files /dev/null and b/src/images/configurationSettings.png differ
diff --git a/src/js/background.js b/src/js/background.js
index eddb7b2..0c60a71 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -1,9 +1,9 @@
-import { parseServices, createMatcher } from 'utils/urlMatcher'
+import { createMatcher } from "utils/urlMatcher"
import remoteServices from "./remoteServices"
-const services = parseServices(remoteServices)
-const matcher = createMatcher(services)
+const matcher = createMatcher(remoteServices)
const { version } = chrome.runtime.getManifest()
+const registeredTabIds = new Set()
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// run only after the page is fully loaded
@@ -14,19 +14,42 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
const service = matcher(tab.url)
if (service) {
+ registeredTabIds.add(tabId)
chrome.storage.sync.get(
["subdomain", "apiKey"],
({ subdomain, apiKey }) => {
- const settings = { subdomain, apiKey, version }
- const payload = { serviceKey: service.key, settings }
- chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
- console.log("bubble mounted")
- })
+ const payload = { subdomain, apiKey, version }
+ chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload })
}
)
} else {
- chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }, () => {
- console.log("bubble unmounted")
- })
+ registeredTabIds.delete(tabId)
+ chrome.tabs.sendMessage(tabId, { type: "unmountBubble" })
+ }
+})
+
+chrome.tabs.onRemoved.addListener(tabId => registeredTabIds.delete(tabId))
+
+chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => {
+ if (areaName === "sync" && (apiKey || subdomain)) {
+ chrome.storage.sync.get(
+ ["subdomain", "apiKey"],
+ ({ subdomain, apiKey }) => {
+ const payload = { subdomain, apiKey, version }
+ for (let tabId of registeredTabIds.values()) {
+ chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload })
+ }
+ }
+ )
+ }
+})
+
+chrome.runtime.onMessage.addListener(({ type }) => {
+ switch (type) {
+ case "openOptions": {
+ chrome.tabs.create({
+ url: `chrome://extensions/?options=${chrome.runtime.id}`
+ })
+ }
}
})
diff --git a/src/js/components/Bubble.js b/src/js/components/Bubble.js
index 86643b8..82740b2 100644
--- a/src/js/components/Bubble.js
+++ b/src/js/components/Bubble.js
@@ -2,9 +2,10 @@ import React, { Component } from "react"
import PropTypes from "prop-types"
import ApiClient from "api/Client"
import Modal, { Content } from "components/Modal"
+import MissingConfigurationError from "components/MissingConfigurationError"
import Form from "components/Form"
-import { observable, computed } from "mobx"
-import { observer } from "mobx-react"
+import { observable, computed, reaction } from "mobx"
+import { observer, disposeOnUnmount } from "mobx-react"
import logoUrl from "images/logo.png"
import {
findLastProject,
@@ -12,6 +13,7 @@ import {
groupedProjectOptions,
currentDate
} from "utils"
+import { head } from "lodash"
@observer
class Bubble extends Component {
@@ -33,19 +35,20 @@ class Bubble extends Component {
#apiClient;
- @observable isLoading = true;
+ @observable isLoading = false;
@observable isOpen = false;
@observable projects;
@observable lastProjectId;
@observable lastTaskId;
@observable changeset = {};
+ @observable errors = {};
@computed get changesetWithDefaults() {
const { service } = this.props
const project =
findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
- this.projects[0]
+ head(this.projects)
const defaults = {
id: service.id,
@@ -86,9 +89,17 @@ class Bubble extends Component {
}
componentDidMount() {
- const { settings } = this.props
- this.#apiClient = new ApiClient(settings)
- this.fetchData()
+ disposeOnUnmount(
+ this,
+ reaction(
+ () =>
+ this.hasMissingConfiguration() ? null : this.props.settings,
+ this.fetchData,
+ {
+ fireImmediately: true
+ }
+ )
+ )
window.addEventListener("keydown", this.handleKeyDown)
}
@@ -104,16 +115,28 @@ class Bubble extends Component {
this.isOpen = false
};
- fetchData = () => {
+ hasMissingConfiguration = () => {
+ const { settings } = this.props
+ return ["subdomain", "apiKey", "version"].some(key => !settings[key])
+ };
+
+ fetchData = settings => {
+ if (!settings) {
+ return
+ }
+
+ this.isLoading = true
+
+ this.#apiClient = new ApiClient(settings)
this.#apiClient
.projects()
.then(({ data }) => {
this.projects = groupedProjectOptions(data.projects)
this.lastProjectId = data.last_project_id
this.lastTaskId = data.lastTaskId
- this.isLoading = false
})
.catch(console.error)
+ .finally(() => (this.isLoading = false))
};
// EVENT HANDLERS -----------------------------------------------------------
@@ -151,6 +174,23 @@ class Bubble extends Component {
return null
}
+ let content
+ if (this.hasMissingConfiguration()) {
+ content =
+ } else if (this.isOpen) {
+ content = (
+
+ )
+ } else {
+ content = null
+ }
+
return (
<>
-
{this.isOpen && (
-
-
-
+ {content}
)}
>
diff --git a/src/js/components/MissingConfigurationError.js b/src/js/components/MissingConfigurationError.js
new file mode 100644
index 0000000..d57bacd
--- /dev/null
+++ b/src/js/components/MissingConfigurationError.js
@@ -0,0 +1,22 @@
+import React from "react"
+import configurationSettingsUrl from "images/configurationSettings.png"
+
+const MissingConfigurationError = () => (
+
+
Fehlende Konfiguration
+
+ Bitte trage deine Internetadresse und deinen API-Schlüssel in den
+ Einstellungen der MOCO Browser-Erweiterung ein. Deinen API-Key findest du
+ in der MOCO App in deinem Profil im Register "Integrationen".
+
+
chrome.runtime.sendMessage({ type: "openOptions" })}>
+ Einstellungen öffnen
+
+
+
+)
+
+export default MissingConfigurationError
diff --git a/src/js/content.js b/src/js/content.js
index d49f823..9f78ac5 100644
--- a/src/js/content.js
+++ b/src/js/content.js
@@ -2,10 +2,13 @@ import { createElement } from "react"
import ReactDOM from "react-dom"
import Bubble from "./components/Bubble"
import services from "remoteServices"
-import { createEnhancer } from "utils/urlMatcher"
+import { parseServices, createMatcher, createEnhancer } from "utils/urlMatcher"
+import remoteServices from "./remoteServices"
+import { pipe } from 'lodash/fp'
import "../css/main.scss"
-const serviceEnhancer = createEnhancer(window.document)(services)
+const matcher = createMatcher(remoteServices)
+const serviceEnhancer = createEnhancer(window.document)
chrome.runtime.onMessage.addListener(({ type, payload }) => {
switch (type) {
@@ -19,7 +22,16 @@ chrome.runtime.onMessage.addListener(({ type, payload }) => {
}
})
-const mountBubble = ({ serviceKey, settings }) => {
+const mountBubble = (settings) => {
+ const service = pipe(
+ matcher,
+ serviceEnhancer(window.location.href)
+ )(window.location.href)
+
+ if (!service) {
+ return
+ }
+
if (!document.getElementById("moco-bx-container")) {
const domContainer = document.createElement("div")
domContainer.setAttribute("id", "moco-bx-container")
@@ -32,7 +44,6 @@ const mountBubble = ({ serviceKey, settings }) => {
document.body.appendChild(domBubble)
}
- const service = serviceEnhancer(serviceKey, window.location.href)
ReactDOM.render(
createElement(Bubble, { service, settings }),
document.getElementById("moco-bx-bubble")
diff --git a/src/js/utils/index.js b/src/js/utils/index.js
index c334de3..49d0c71 100644
--- a/src/js/utils/index.js
+++ b/src/js/utils/index.js
@@ -60,3 +60,5 @@ export const trace = curry((tag, value) => {
export const currentDate = (locale = "de") =>
format(new Date(), "YYYY-MM-DD", { locale })
+
+export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`
diff --git a/src/js/utils/urlMatcher.js b/src/js/utils/urlMatcher.js
index 50c948f..9fedb6a 100644
--- a/src/js/utils/urlMatcher.js
+++ b/src/js/utils/urlMatcher.js
@@ -13,7 +13,7 @@ const createEvaluator = args => fnOrValue => {
return fnOrValue
}
-export const parseServices = compose(
+const parseServices = compose(
map(([key, config]) => ({
...config,
key,
@@ -22,9 +22,11 @@ export const parseServices = compose(
toPairs
)
-export const createEnhancer = document => services => (key, url) => {
- const service = services[key]
- service.key = key
+export const createEnhancer = document => url => service => {
+ if (!service) {
+ return
+ }
+
const route = new Route(service.urlPattern)
const match = route.match(url)
const args = [document, service, match]
@@ -32,14 +34,15 @@ export const createEnhancer = document => services => (key, url) => {
return {
...service,
- key,
url,
id: evaluate(service.id) || match.id,
description: evaluate(service.description),
projectId: evaluate(service.projectId),
- taskId: evaluate(service.taskId),
+ taskId: evaluate(service.taskId)
}
}
-export const createMatcher = services => url =>
- services.find(service => service.route.match(url))
+export const createMatcher = remoteServices => {
+ const services = parseServices(remoteServices)
+ return url => services.find(service => service.route.match(url))
+}
diff --git a/test/utils/urlMatcher.test.js b/test/utils/urlMatcher.test.js
index a9688de..5c30935 100644
--- a/test/utils/urlMatcher.test.js
+++ b/test/utils/urlMatcher.test.js
@@ -1,64 +1,56 @@
-import { remoteServices } from '../data'
-import { parseServices, createMatcher, createEnhancer } from '../../src/js/utils/urlMatcher'
-import Route from "route-parser"
+import { remoteServices } from "../data"
+import { createMatcher, createEnhancer } from "../../src/js/utils/urlMatcher"
-describe('utils', () => {
+describe("utils", () => {
describe("urlMatcher", () => {
- describe("parseServices", () => {
- it("parses the services", () => {
- const services = parseServices(remoteServices)
+ let matcher
- let service = services[0]
- expect(service.key).toEqual("github-pr")
- expect(service.name).toEqual("github")
- expect(service.route).toBeInstanceOf(Route)
-
- service = services[1]
- expect(service.key).toEqual("jira-cloud")
- expect(service.name).toEqual("jira")
- expect(service.route).toBeInstanceOf(Route)
- })
+ beforeEach(() => {
+ matcher = createMatcher(remoteServices)
})
describe("createMatcher", () => {
- let services, matcher
-
- beforeEach(() => {
- services = parseServices(remoteServices)
- matcher = createMatcher(services)
+ it("matches host and path", () => {
+ const service = matcher(
+ "https://github.com/hundertzehn/mocoapp/pull/123"
+ )
+ expect(service.key).toEqual("github-pr")
+ expect(service.name).toEqual("github")
})
- it('matches host and path', () => {
- const service = matcher('https://github.com/hundertzehn/mocoapp/pull/123')
- expect(service.key).toEqual('github-pr')
- expect(service.name).toEqual('github')
+ it("matches query string", () => {
+ const service = matcher(
+ "https://cloud.jira.com/browse?project=mocoapp&issue=1234"
+ )
+ expect(service.key).toEqual("jira-cloud")
+ expect(service.name).toEqual("jira")
})
- it('matches query string', () => {
- const service = matcher('https://cloud.jira.com/browse?project=mocoapp&issue=1234')
- expect(service.key).toEqual('jira-cloud')
- expect(service.name).toEqual('jira')
- })
-
- it('does not match different host', () => {
- const service = matcher('https://trello.com/hundertzehn/mocoapp/pull/123')
+ it("does not match different host", () => {
+ const service = matcher(
+ "https://trello.com/hundertzehn/mocoapp/pull/123"
+ )
expect(service).toBeFalsy()
})
})
describe("createEnhancer", () => {
it("enhances a services", () => {
- const url = 'https://github.com/hundertzehn/mocoapp/pull/123'
+ const url = "https://github.com/hundertzehn/mocoapp/pull/123"
const document = {
- querySelector: jest.fn().mockReturnValue({ textContent: '[4321] Foo' })
+ querySelector: jest
+ .fn()
+ .mockReturnValue({ textContent: "[4321] Foo" })
}
- const enhancedService = createEnhancer(document)(remoteServices)('github-pr', url)
- expect(enhancedService.id).toEqual( 'hundertzehn-mocoapp-github-pr-123')
- expect(enhancedService.description).toEqual('This is always the same text')
- expect(enhancedService.projectId).toEqual('4321')
+ const service = matcher(url)
+ const enhancedService = createEnhancer(document)(url)(service)
+ expect(enhancedService.id).toEqual("hundertzehn-mocoapp-github-pr-123")
+ expect(enhancedService.description).toEqual(
+ "This is always the same text"
+ )
+ expect(enhancedService.projectId).toEqual("4321")
expect(enhancedService.taskId).toBe(undefined)
})
})
})
})
-
diff --git a/webpack.config.js b/webpack.config.js
index 55386ba..21f0848 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -38,7 +38,7 @@ module.exports = {
}
},
{
- test: /\.(png)$/,
+ test: /\.(jpg|png)$/,
loader: "file-loader",
options: {
name: "[path][name].[ext]"