Mount Bubble only for configured services
This commit is contained in:
@@ -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
1
jest.config.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = {}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
12
src/js/config.js
Normal 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
47
src/js/content.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
48
test/services/DomainCheck.test.js
Normal file
48
test/services/DomainCheck.test.js
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
};
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user