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". +

+ + Browser extension configuration settings +
+) + +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]"