From a4f30496717a3d3b257377039a84d0c8763caf89 Mon Sep 17 00:00:00 2001 From: Manuel Bouza Date: Mon, 11 Feb 2019 16:40:22 +0100 Subject: [PATCH] Show error page on missing configuration --- src/css/_form.scss | 2 +- src/images/configurationSettings.png | Bin 0 -> 16698 bytes src/js/background.js | 45 ++++++++--- src/js/components/Bubble.js | 69 +++++++++++----- .../components/MissingConfigurationError.js | 22 ++++++ src/js/content.js | 19 ++++- src/js/utils/index.js | 2 + src/js/utils/urlMatcher.js | 19 +++-- test/utils/urlMatcher.test.js | 74 ++++++++---------- webpack.config.js | 2 +- 10 files changed, 169 insertions(+), 85 deletions(-) create mode 100644 src/images/configurationSettings.png create mode 100644 src/js/components/MissingConfigurationError.js 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 0000000000000000000000000000000000000000..ebdf99d2f6818b5824e70bc26930daad44c41206 GIT binary patch literal 16698 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 { // 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]"