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