Mount Bubble only for configured services

This commit is contained in:
Manuel Bouza
2019-02-05 18:19:11 +01:00
parent ed711c954e
commit 73e0c47833
16 changed files with 1331 additions and 116 deletions

View File

@@ -23,6 +23,7 @@
}, },
"rules": { "rules": {
"strict": 0, "strict": 0,
"semi": ["error", "never"],
"array-callback-return": "warn", "array-callback-return": "warn",
"getter-return": "warn", "getter-return": "warn",
"no-const-assign": "warn", "no-const-assign": "warn",

1
jest.config.js Normal file
View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -5,6 +5,8 @@
"main": "bundle.js", "main": "bundle.js",
"scripts": { "scripts": {
"start": "node_modules/.bin/webpack --watch", "start": "node_modules/.bin/webpack --watch",
"test": "node_modules/.bin/jest",
"test:watch": "node_modules/.bin/jest --watch",
"release": "copyfiles main.css main.min.js background.min.js manifest.json popup.html options.html node_modules/jquery/dist/jquery.min.js node_modules/select2/select2.js src/images/* release" "release": "copyfiles main.css main.min.js background.min.js manifest.json popup.html options.html node_modules/jquery/dist/jquery.min.js node_modules/select2/select2.js src/images/* release"
}, },
"dependencies": { "dependencies": {
@@ -34,6 +36,7 @@
"eslint-plugin-react": "^7.11.1", "eslint-plugin-react": "^7.11.1",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",

View File

@@ -1,16 +1,24 @@
import DomainCheck from "./services/DomainCheck"; import DomainCheck from "./services/DomainCheck"
import config from "./config"
const domainCheck = new DomainCheck(config)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// inject files only after the page is fully loaded // inject files only after the page is fully loaded
if (changeInfo.status != "complete") return; if (changeInfo.status != "complete") {
return
}
// inject files only for supported websites // inject files only for supported services
const domainCheck = new DomainCheck(tab.url); const service = domainCheck.match(tab.url)
if (!domainCheck.hasMatch) return;
chrome.tabs.executeScript(tabId, { file: "/bubble.js" }, () => { if (service) {
// chrome.tabs.executeScript(tabId, {file: "/popup.js"}, () => { chrome.tabs.sendMessage(tabId, { type: "mountBubble", service }, () => {
console.log("injected bubble.js"); console.log("bubble mounted")
// }) })
}); } else {
}); chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }, () => {
console.log("bubble unmounted")
})
}
})

View File

@@ -1,17 +0,0 @@
import { createElement } from "react";
import ReactDOM from "react-dom";
import Bubble from "./components/Bubble";
import "../css/main.scss";
// don't initiate bubble twice
if (!document.querySelector("#moco-bx-container")) {
const domContainer = document.createElement("div");
domContainer.setAttribute("id", "moco-bx-container");
document.body.appendChild(domContainer);
const domBubble = document.createElement("div");
domBubble.setAttribute("id", "moco-bx-bubble");
document.body.appendChild(domBubble);
ReactDOM.render(createElement(Bubble), domBubble);
}

View File

@@ -1,21 +1,25 @@
import React, { Component } from "react"; import React, { Component } from "react"
import Modal, { Content } from "components/Modal"; import Modal, { Content } from "components/Modal"
import Form from "components/Form"; import Form from "components/Form"
import { observable } from "mobx"; import { observable } from "mobx"
import { observer } from "mobx-react"; import { observer } from "mobx-react"
import logoUrl from "../../images/logo.png"; import logoUrl from "../../images/logo.png"
@observer @observer
class Bubble extends Component { class Bubble extends Component {
@observable open = false; @observable open = false
onOpen = _e => { onOpen = _event => {
this.open = true; this.open = true
}; }
onClose = _e => { onClose = _event => {
this.open = false; this.open = false
}; }
componentDidMount() {
console.log(this.props.service)
}
// RENDER ------------------------------------------------------------------- // RENDER -------------------------------------------------------------------
@@ -37,8 +41,8 @@ class Bubble extends Component {
</Modal> </Modal>
)} )}
</> </>
); )
} }
} }
export default Bubble; export default Bubble

View File

@@ -1,25 +1,25 @@
import React, { Component } from 'react' import React, { Component } from "react"
import { createPortal } from 'react-dom' import { createPortal } from "react-dom"
import PropTypes from 'prop-types' import PropTypes from "prop-types"
class Modal extends Component { class Modal extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.el = document.createElement('div') this.el = document.createElement("div")
this.el.setAttribute('class', 'moco-bx-modal') this.el.setAttribute("class", "moco-bx-modal")
} }
componentDidMount() { componentDidMount() {
const modalRoot = document.getElementById('moco-bx-container') const modalRoot = document.getElementById("moco-bx-container")
modalRoot.appendChild(this.el) modalRoot.appendChild(this.el)
} }
componentWillUnmount() { componentWillUnmount() {
const modalRoot = document.getElementById('moco-bx-container') const modalRoot = document.getElementById("moco-bx-container")
modalRoot.removeChild(this.el) modalRoot.removeChild(this.el)
} }
@@ -30,10 +30,12 @@ class Modal extends Component {
} }
} }
export function Content({children}) { export function Content({ children }) {
return ( return <div className="moco-bx-modal-content">{children}</div>
<div className="moco-bx-modal-content">{children}</div> }
)
Content.propTypes = {
children: PropTypes.node
} }
export default Modal export default Modal

View File

@@ -1,26 +1,26 @@
import React, { Component } from "react"; import React, { Component } from "react"
import { observable } from "mobx"; import { observable } from "mobx"
import { observer } from "mobx-react"; import { observer } from "mobx-react"
@observer @observer
class Setup extends Component { class Setup extends Component {
@observable loading = true; @observable loading = true
@observable subdomain = ""; @observable subdomain = ""
@observable apiKey = ""; @observable apiKey = ""
componentDidMount() { componentDidMount() {
chrome.storage.sync.get(null, store => { chrome.storage.sync.get(null, store => {
this.loading = false; this.loading = false
this.subdomain = store.subdomain || ""; this.subdomain = store.subdomain || ""
this.apiKey = store.api_key || ""; this.apiKey = store.api_key || ""
}); })
} }
// EVENTS // EVENTS
onChange = event => { onChange = event => {
this[event.target.name] = event.target.value; this[event.target.name] = event.target.value
}; }
onSubmit = _event => { onSubmit = _event => {
chrome.storage.sync.set( chrome.storage.sync.set(
@@ -29,13 +29,13 @@ class Setup extends Component {
api_key: this.apiKey.trim() api_key: this.apiKey.trim()
}, },
() => window.close() () => window.close()
); )
}; }
// RENDER ------------------------------------------------------------------- // RENDER -------------------------------------------------------------------
render() { render() {
if (this.loading) return null; if (this.loading) return null
return ( return (
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
@@ -67,8 +67,8 @@ class Setup extends Component {
</div> </div>
<button>Save</button> <button>Save</button>
</form> </form>
); )
} }
} }
export default Setup; export default Setup

12
src/js/config.js Normal file
View File

@@ -0,0 +1,12 @@
export default {
services: [
{
name: "github",
urlPattern: "https://github.com/:org/:repo/pull/:id(/:tab)",
description: (document, { org, repo, id }) =>
`${org}/${repo}/${id} - ${document
.querySelector(".gh-header-title")
.textContent.trim()}`
}
]
}

47
src/js/content.js Normal file
View File

@@ -0,0 +1,47 @@
import { createElement } from "react"
import ReactDOM from "react-dom"
import Bubble from "./components/Bubble"
import "../css/main.scss"
chrome.runtime.onMessage.addListener(({ type, service }) => {
switch (type) {
case "mountBubble": {
return mountBubble(service)
}
case "unmountBubble": {
return unmountBubble()
}
}
})
const mountBubble = service => {
if (document.getElementById("moco-bx-bubble")) {
return
}
const domContainer = document.createElement("div")
domContainer.setAttribute("id", "moco-bx-container")
document.body.appendChild(domContainer)
const domBubble = document.createElement("div")
domBubble.setAttribute("id", "moco-bx-bubble")
document.body.appendChild(domBubble)
ReactDOM.render(createElement(Bubble, { service }), domBubble)
}
const unmountBubble = () => {
const domBubble = document.getElementById("moco-bx-bubble")
const domContainer = document.getElementById("moco-bx-container")
if (domBubble) {
ReactDOM.unmountComponentAtNode(domBubble)
domBubble.remove()
}
if (domContainer) {
ReactDOM.unmountComponentAtNode(domContainer)
domContainer.remove()
}
}

View File

@@ -1,7 +1,7 @@
import { createElement } from "react"; import { createElement } from "react"
import ReactDOM from "react-dom"; import ReactDOM from "react-dom"
import Setup from "./components/Setup"; import Setup from "./components/Setup"
import "../css/main.scss"; import "../css/main.scss"
const domContainer = document.querySelector("#moco-bx-container"); const domContainer = document.querySelector("#moco-bx-container")
ReactDOM.render(createElement(Setup), domContainer); ReactDOM.render(createElement(Setup), domContainer)

View File

@@ -1,11 +1,31 @@
import Route from "route-parser"
class DomainCheck { class DomainCheck {
constructor(url) { #services;
this.url = url
constructor(config) {
this.#services = config.services.map(service => ({
...service,
route: new Route(service.urlPattern),
}))
} }
get hasMatch() { #findService = url =>
return this.url.match(/github/) || this.url.match(/trello/) || this.url.match(/mocoapp/) this.#services.find(service => service.route.match(url));
match(url) {
const service = this.#findService(url)
if (!service) {
return false
}
return {
...service,
match: service.route.match(url),
}
} }
} }
export default DomainCheck export default DomainCheck

View File

@@ -25,6 +25,12 @@
"background": { "background": {
"scripts": ["background.js"] "scripts": ["background.js"]
}, },
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"browser_action": { "browser_action": {
"default_icon": "src/images/logo.png", "default_icon": "src/images/logo.png",
"default_title": "MOCO Time Tracking", "default_title": "MOCO Time Tracking",

View File

@@ -0,0 +1,48 @@
import DomainCheck from '../../src/js/services/DomainCheck'
describe('services', () => {
describe('DomainCheck', () => {
const config = {
services: [
{
name: 'github',
urlPattern: 'https://github.com/:org/:repo/pull/:id',
},
{
name: 'jira',
urlPattern: 'https://support.jira.com/browse?project=:project&issue=:id'
}
]
}
let domainCheck
beforeAll(() => {
domainCheck = new DomainCheck(config)
})
it('matches host and path', () => {
const service = domainCheck.match('https://github.com/hundertzehn/mocoapp/pull/123')
expect(service.name).toEqual('github')
expect(service.match).toEqual({
org: 'hundertzehn',
repo: 'mocoapp',
id: '123',
})
})
it('matches query string', () => {
const service = domainCheck.match('https://support.jira.com/browse?project=mocoapp&issue=1234')
expect(service.name).toEqual('jira')
expect(service.match).toEqual({
project: 'mocoapp',
id: '1234',
})
})
it('does not match different host', () => {
const service = domainCheck.match('https://trello.com/hundertzehn/mocoapp/pull/123')
expect(service).toBeFalsy()
})
})
})

View File

@@ -1,12 +1,12 @@
const path = require("path"); const path = require("path")
const CleanWebpackPlugin = require("clean-webpack-plugin"); const CleanWebpackPlugin = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports = { module.exports = {
entry: { entry: {
background: "./src/js/background.js", background: "./src/js/background.js",
bubble: "./src/js/bubble.js", content: "./src/js/content.js",
options: "./src/js/options.js", options: "./src/js/options.js",
popup: "./src/js/popup.js" popup: "./src/js/popup.js"
}, },
@@ -60,7 +60,7 @@ module.exports = {
version: process.env.npm_package_version, version: process.env.npm_package_version,
...JSON.parse(content.toString()) ...JSON.parse(content.toString())
}) })
); )
} }
} }
]), ]),
@@ -80,4 +80,4 @@ module.exports = {
// https://stackoverflow.com/a/49100966 // https://stackoverflow.com/a/49100966
devtool: "none", devtool: "none",
mode: process.env.NODE_ENV || "development" mode: process.env.NODE_ENV || "development"
}; }

1134
yarn.lock

File diff suppressed because it is too large Load Diff