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": {
"strict": 0,
"semi": ["error", "never"],
"array-callback-return": "warn",
"getter-return": "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",
"scripts": {
"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"
},
"dependencies": {
@@ -34,6 +36,7 @@
"eslint-plugin-react": "^7.11.1",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0",
"node-sass": "^4.11.0",
"sass-loader": "^7.1.0",
"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) => {
// 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
const domainCheck = new DomainCheck(tab.url);
if (!domainCheck.hasMatch) return;
// inject files only for supported services
const service = domainCheck.match(tab.url)
chrome.tabs.executeScript(tabId, { file: "/bubble.js" }, () => {
// chrome.tabs.executeScript(tabId, {file: "/popup.js"}, () => {
console.log("injected bubble.js");
// })
});
});
if (service) {
chrome.tabs.sendMessage(tabId, { type: "mountBubble", service }, () => {
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 Modal, { Content } from "components/Modal";
import Form from "components/Form";
import { observable } from "mobx";
import { observer } from "mobx-react";
import logoUrl from "../../images/logo.png";
import React, { Component } from "react"
import Modal, { Content } from "components/Modal"
import Form from "components/Form"
import { observable } from "mobx"
import { observer } from "mobx-react"
import logoUrl from "../../images/logo.png"
@observer
class Bubble extends Component {
@observable open = false;
@observable open = false
onOpen = _e => {
this.open = true;
};
onOpen = _event => {
this.open = true
}
onClose = _e => {
this.open = false;
};
onClose = _event => {
this.open = false
}
componentDidMount() {
console.log(this.props.service)
}
// RENDER -------------------------------------------------------------------
@@ -37,8 +41,8 @@ class Bubble extends Component {
</Modal>
)}
</>
);
)
}
}
export default Bubble;
export default Bubble

View File

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

View File

@@ -1,26 +1,26 @@
import React, { Component } from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import React, { Component } from "react"
import { observable } from "mobx"
import { observer } from "mobx-react"
@observer
class Setup extends Component {
@observable loading = true;
@observable subdomain = "";
@observable apiKey = "";
@observable loading = true
@observable subdomain = ""
@observable apiKey = ""
componentDidMount() {
chrome.storage.sync.get(null, store => {
this.loading = false;
this.subdomain = store.subdomain || "";
this.apiKey = store.api_key || "";
});
this.loading = false
this.subdomain = store.subdomain || ""
this.apiKey = store.api_key || ""
})
}
// EVENTS
onChange = event => {
this[event.target.name] = event.target.value;
};
this[event.target.name] = event.target.value
}
onSubmit = _event => {
chrome.storage.sync.set(
@@ -29,13 +29,13 @@ class Setup extends Component {
api_key: this.apiKey.trim()
},
() => window.close()
);
};
)
}
// RENDER -------------------------------------------------------------------
render() {
if (this.loading) return null;
if (this.loading) return null
return (
<form onSubmit={this.onSubmit}>
@@ -67,8 +67,8 @@ class Setup extends Component {
</div>
<button>Save</button>
</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 ReactDOM from "react-dom";
import Setup from "./components/Setup";
import "../css/main.scss";
import { createElement } from "react"
import ReactDOM from "react-dom"
import Setup from "./components/Setup"
import "../css/main.scss"
const domContainer = document.querySelector("#moco-bx-container");
ReactDOM.render(createElement(Setup), domContainer);
const domContainer = document.querySelector("#moco-bx-container")
ReactDOM.render(createElement(Setup), domContainer)

View File

@@ -1,11 +1,31 @@
import Route from "route-parser"
class DomainCheck {
constructor(url) {
this.url = url
#services;
constructor(config) {
this.#services = config.services.map(service => ({
...service,
route: new Route(service.urlPattern),
}))
}
get hasMatch() {
return this.url.match(/github/) || this.url.match(/trello/) || this.url.match(/mocoapp/)
#findService = url =>
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

View File

@@ -25,6 +25,12 @@
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"browser_action": {
"default_icon": "src/images/logo.png",
"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 CleanWebpackPlugin = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path")
const CleanWebpackPlugin = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports = {
entry: {
background: "./src/js/background.js",
bubble: "./src/js/bubble.js",
content: "./src/js/content.js",
options: "./src/js/options.js",
popup: "./src/js/popup.js"
},
@@ -60,7 +60,7 @@ module.exports = {
version: process.env.npm_package_version,
...JSON.parse(content.toString())
})
);
)
}
}
]),
@@ -80,4 +80,4 @@ module.exports = {
// https://stackoverflow.com/a/49100966
devtool: "none",
mode: process.env.NODE_ENV || "development"
};
}

1134
yarn.lock

File diff suppressed because it is too large Load Diff