Show error page on missing configuration
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-group-addon {
|
.input-group-addon {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.25rem 0.5rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #555555;
|
color: #555555;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
BIN
src/images/configurationSettings.png
Normal file
BIN
src/images/configurationSettings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1,9 +1,9 @@
|
|||||||
import { parseServices, createMatcher } from 'utils/urlMatcher'
|
import { createMatcher } from "utils/urlMatcher"
|
||||||
import remoteServices from "./remoteServices"
|
import remoteServices from "./remoteServices"
|
||||||
|
|
||||||
const services = parseServices(remoteServices)
|
const matcher = createMatcher(remoteServices)
|
||||||
const matcher = createMatcher(services)
|
|
||||||
const { version } = chrome.runtime.getManifest()
|
const { version } = chrome.runtime.getManifest()
|
||||||
|
const registeredTabIds = new Set()
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
// run only after the page is fully loaded
|
// run only after the page is fully loaded
|
||||||
@@ -14,19 +14,42 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|||||||
const service = matcher(tab.url)
|
const service = matcher(tab.url)
|
||||||
|
|
||||||
if (service) {
|
if (service) {
|
||||||
|
registeredTabIds.add(tabId)
|
||||||
chrome.storage.sync.get(
|
chrome.storage.sync.get(
|
||||||
["subdomain", "apiKey"],
|
["subdomain", "apiKey"],
|
||||||
({ subdomain, apiKey }) => {
|
({ subdomain, apiKey }) => {
|
||||||
const settings = { subdomain, apiKey, version }
|
const payload = { subdomain, apiKey, version }
|
||||||
const payload = { serviceKey: service.key, settings }
|
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload })
|
||||||
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
|
|
||||||
console.log("bubble mounted")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }, () => {
|
registeredTabIds.delete(tabId)
|
||||||
console.log("bubble unmounted")
|
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}`
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import React, { Component } from "react"
|
|||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
import ApiClient from "api/Client"
|
import ApiClient from "api/Client"
|
||||||
import Modal, { Content } from "components/Modal"
|
import Modal, { Content } from "components/Modal"
|
||||||
|
import MissingConfigurationError from "components/MissingConfigurationError"
|
||||||
import Form from "components/Form"
|
import Form from "components/Form"
|
||||||
import { observable, computed } from "mobx"
|
import { observable, computed, reaction } from "mobx"
|
||||||
import { observer } from "mobx-react"
|
import { observer, disposeOnUnmount } from "mobx-react"
|
||||||
import logoUrl from "images/logo.png"
|
import logoUrl from "images/logo.png"
|
||||||
import {
|
import {
|
||||||
findLastProject,
|
findLastProject,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
groupedProjectOptions,
|
groupedProjectOptions,
|
||||||
currentDate
|
currentDate
|
||||||
} from "utils"
|
} from "utils"
|
||||||
|
import { head } from "lodash"
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Bubble extends Component {
|
class Bubble extends Component {
|
||||||
@@ -33,19 +35,20 @@ class Bubble extends Component {
|
|||||||
|
|
||||||
#apiClient;
|
#apiClient;
|
||||||
|
|
||||||
@observable isLoading = true;
|
@observable isLoading = false;
|
||||||
@observable isOpen = false;
|
@observable isOpen = false;
|
||||||
@observable projects;
|
@observable projects;
|
||||||
@observable lastProjectId;
|
@observable lastProjectId;
|
||||||
@observable lastTaskId;
|
@observable lastTaskId;
|
||||||
@observable changeset = {};
|
@observable changeset = {};
|
||||||
|
@observable errors = {};
|
||||||
|
|
||||||
@computed get changesetWithDefaults() {
|
@computed get changesetWithDefaults() {
|
||||||
const { service } = this.props
|
const { service } = this.props
|
||||||
|
|
||||||
const project =
|
const project =
|
||||||
findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
|
findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
|
||||||
this.projects[0]
|
head(this.projects)
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
id: service.id,
|
id: service.id,
|
||||||
@@ -86,9 +89,17 @@ class Bubble extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { settings } = this.props
|
disposeOnUnmount(
|
||||||
this.#apiClient = new ApiClient(settings)
|
this,
|
||||||
this.fetchData()
|
reaction(
|
||||||
|
() =>
|
||||||
|
this.hasMissingConfiguration() ? null : this.props.settings,
|
||||||
|
this.fetchData,
|
||||||
|
{
|
||||||
|
fireImmediately: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
window.addEventListener("keydown", this.handleKeyDown)
|
window.addEventListener("keydown", this.handleKeyDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +115,28 @@ class Bubble extends Component {
|
|||||||
this.isOpen = false
|
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
|
this.#apiClient
|
||||||
.projects()
|
.projects()
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
this.projects = groupedProjectOptions(data.projects)
|
this.projects = groupedProjectOptions(data.projects)
|
||||||
this.lastProjectId = data.last_project_id
|
this.lastProjectId = data.last_project_id
|
||||||
this.lastTaskId = data.lastTaskId
|
this.lastTaskId = data.lastTaskId
|
||||||
this.isLoading = false
|
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
.finally(() => (this.isLoading = false))
|
||||||
};
|
};
|
||||||
|
|
||||||
// EVENT HANDLERS -----------------------------------------------------------
|
// EVENT HANDLERS -----------------------------------------------------------
|
||||||
@@ -151,17 +174,11 @@ class Bubble extends Component {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
let content
|
||||||
<>
|
if (this.hasMissingConfiguration()) {
|
||||||
<img
|
content = <MissingConfigurationError />
|
||||||
onClick={this.open}
|
} else if (this.isOpen) {
|
||||||
src={chrome.extension.getURL(logoUrl)}
|
content = (
|
||||||
width="50%"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this.isOpen && (
|
|
||||||
<Modal>
|
|
||||||
<Content>
|
|
||||||
<Form
|
<Form
|
||||||
projects={this.projects}
|
projects={this.projects}
|
||||||
changeset={this.changesetWithDefaults}
|
changeset={this.changesetWithDefaults}
|
||||||
@@ -169,7 +186,21 @@ class Bubble extends Component {
|
|||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onSubmit={this.handleSubmit}
|
onSubmit={this.handleSubmit}
|
||||||
/>
|
/>
|
||||||
</Content>
|
)
|
||||||
|
} else {
|
||||||
|
content = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
onClick={this.open}
|
||||||
|
src={chrome.extension.getURL(logoUrl)}
|
||||||
|
width="50%"
|
||||||
|
/>
|
||||||
|
{this.isOpen && (
|
||||||
|
<Modal>
|
||||||
|
<Content>{content}</Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
22
src/js/components/MissingConfigurationError.js
Normal file
22
src/js/components/MissingConfigurationError.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react"
|
||||||
|
import configurationSettingsUrl from "images/configurationSettings.png"
|
||||||
|
|
||||||
|
const MissingConfigurationError = () => (
|
||||||
|
<div>
|
||||||
|
<h2>Fehlende Konfiguration</h2>
|
||||||
|
<p>
|
||||||
|
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".
|
||||||
|
</p>
|
||||||
|
<button onClick={() => chrome.runtime.sendMessage({ type: "openOptions" })}>
|
||||||
|
Einstellungen öffnen
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={chrome.extension.getURL(configurationSettingsUrl)}
|
||||||
|
alt="Browser extension configuration settings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default MissingConfigurationError
|
||||||
@@ -2,10 +2,13 @@ import { createElement } from "react"
|
|||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
import Bubble from "./components/Bubble"
|
import Bubble from "./components/Bubble"
|
||||||
import services from "remoteServices"
|
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"
|
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 }) => {
|
chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
||||||
switch (type) {
|
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")) {
|
if (!document.getElementById("moco-bx-container")) {
|
||||||
const domContainer = document.createElement("div")
|
const domContainer = document.createElement("div")
|
||||||
domContainer.setAttribute("id", "moco-bx-container")
|
domContainer.setAttribute("id", "moco-bx-container")
|
||||||
@@ -32,7 +44,6 @@ const mountBubble = ({ serviceKey, settings }) => {
|
|||||||
document.body.appendChild(domBubble)
|
document.body.appendChild(domBubble)
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = serviceEnhancer(serviceKey, window.location.href)
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
createElement(Bubble, { service, settings }),
|
createElement(Bubble, { service, settings }),
|
||||||
document.getElementById("moco-bx-bubble")
|
document.getElementById("moco-bx-bubble")
|
||||||
|
|||||||
@@ -60,3 +60,5 @@ export const trace = curry((tag, value) => {
|
|||||||
|
|
||||||
export const currentDate = (locale = "de") =>
|
export const currentDate = (locale = "de") =>
|
||||||
format(new Date(), "YYYY-MM-DD", { locale })
|
format(new Date(), "YYYY-MM-DD", { locale })
|
||||||
|
|
||||||
|
export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const createEvaluator = args => fnOrValue => {
|
|||||||
return fnOrValue
|
return fnOrValue
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseServices = compose(
|
const parseServices = compose(
|
||||||
map(([key, config]) => ({
|
map(([key, config]) => ({
|
||||||
...config,
|
...config,
|
||||||
key,
|
key,
|
||||||
@@ -22,9 +22,11 @@ export const parseServices = compose(
|
|||||||
toPairs
|
toPairs
|
||||||
)
|
)
|
||||||
|
|
||||||
export const createEnhancer = document => services => (key, url) => {
|
export const createEnhancer = document => url => service => {
|
||||||
const service = services[key]
|
if (!service) {
|
||||||
service.key = key
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const route = new Route(service.urlPattern)
|
const route = new Route(service.urlPattern)
|
||||||
const match = route.match(url)
|
const match = route.match(url)
|
||||||
const args = [document, service, match]
|
const args = [document, service, match]
|
||||||
@@ -32,14 +34,15 @@ export const createEnhancer = document => services => (key, url) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...service,
|
...service,
|
||||||
key,
|
|
||||||
url,
|
url,
|
||||||
id: evaluate(service.id) || match.id,
|
id: evaluate(service.id) || match.id,
|
||||||
description: evaluate(service.description),
|
description: evaluate(service.description),
|
||||||
projectId: evaluate(service.projectId),
|
projectId: evaluate(service.projectId),
|
||||||
taskId: evaluate(service.taskId),
|
taskId: evaluate(service.taskId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createMatcher = services => url =>
|
export const createMatcher = remoteServices => {
|
||||||
services.find(service => service.route.match(url))
|
const services = parseServices(remoteServices)
|
||||||
|
return url => services.find(service => service.route.match(url))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,64 +1,56 @@
|
|||||||
import { remoteServices } from '../data'
|
import { remoteServices } from "../data"
|
||||||
import { parseServices, createMatcher, createEnhancer } from '../../src/js/utils/urlMatcher'
|
import { createMatcher, createEnhancer } from "../../src/js/utils/urlMatcher"
|
||||||
import Route from "route-parser"
|
|
||||||
|
|
||||||
describe('utils', () => {
|
describe("utils", () => {
|
||||||
describe("urlMatcher", () => {
|
describe("urlMatcher", () => {
|
||||||
describe("parseServices", () => {
|
let matcher
|
||||||
it("parses the services", () => {
|
|
||||||
const services = parseServices(remoteServices)
|
|
||||||
|
|
||||||
let service = services[0]
|
beforeEach(() => {
|
||||||
expect(service.key).toEqual("github-pr")
|
matcher = createMatcher(remoteServices)
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createMatcher", () => {
|
describe("createMatcher", () => {
|
||||||
let services, matcher
|
it("matches host and path", () => {
|
||||||
|
const service = matcher(
|
||||||
beforeEach(() => {
|
"https://github.com/hundertzehn/mocoapp/pull/123"
|
||||||
services = parseServices(remoteServices)
|
)
|
||||||
matcher = createMatcher(services)
|
expect(service.key).toEqual("github-pr")
|
||||||
|
expect(service.name).toEqual("github")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('matches host and path', () => {
|
it("matches query string", () => {
|
||||||
const service = matcher('https://github.com/hundertzehn/mocoapp/pull/123')
|
const service = matcher(
|
||||||
expect(service.key).toEqual('github-pr')
|
"https://cloud.jira.com/browse?project=mocoapp&issue=1234"
|
||||||
expect(service.name).toEqual('github')
|
)
|
||||||
|
expect(service.key).toEqual("jira-cloud")
|
||||||
|
expect(service.name).toEqual("jira")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('matches query string', () => {
|
it("does not match different host", () => {
|
||||||
const service = matcher('https://cloud.jira.com/browse?project=mocoapp&issue=1234')
|
const service = matcher(
|
||||||
expect(service.key).toEqual('jira-cloud')
|
"https://trello.com/hundertzehn/mocoapp/pull/123"
|
||||||
expect(service.name).toEqual('jira')
|
)
|
||||||
})
|
|
||||||
|
|
||||||
it('does not match different host', () => {
|
|
||||||
const service = matcher('https://trello.com/hundertzehn/mocoapp/pull/123')
|
|
||||||
expect(service).toBeFalsy()
|
expect(service).toBeFalsy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createEnhancer", () => {
|
describe("createEnhancer", () => {
|
||||||
it("enhances a services", () => {
|
it("enhances a services", () => {
|
||||||
const url = 'https://github.com/hundertzehn/mocoapp/pull/123'
|
const url = "https://github.com/hundertzehn/mocoapp/pull/123"
|
||||||
const document = {
|
const document = {
|
||||||
querySelector: jest.fn().mockReturnValue({ textContent: '[4321] Foo' })
|
querySelector: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ textContent: "[4321] Foo" })
|
||||||
}
|
}
|
||||||
const enhancedService = createEnhancer(document)(remoteServices)('github-pr', url)
|
const service = matcher(url)
|
||||||
expect(enhancedService.id).toEqual( 'hundertzehn-mocoapp-github-pr-123')
|
const enhancedService = createEnhancer(document)(url)(service)
|
||||||
expect(enhancedService.description).toEqual('This is always the same text')
|
expect(enhancedService.id).toEqual("hundertzehn-mocoapp-github-pr-123")
|
||||||
expect(enhancedService.projectId).toEqual('4321')
|
expect(enhancedService.description).toEqual(
|
||||||
|
"This is always the same text"
|
||||||
|
)
|
||||||
|
expect(enhancedService.projectId).toEqual("4321")
|
||||||
expect(enhancedService.taskId).toBe(undefined)
|
expect(enhancedService.taskId).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png)$/,
|
test: /\.(jpg|png)$/,
|
||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
options: {
|
options: {
|
||||||
name: "[path][name].[ext]"
|
name: "[path][name].[ext]"
|
||||||
|
|||||||
Reference in New Issue
Block a user