Enhance service
This commit is contained in:
3
.babelrc
3
.babelrc
@@ -2,6 +2,7 @@
|
|||||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||||
["@babel/plugin-proposal-class-properties", { "loose": true }]
|
["@babel/plugin-proposal-class-properties", { "loose": true }],
|
||||||
|
["@babel/plugin-proposal-optional-chaining"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@babel/core": "^7.2.2",
|
"@babel/core": "^7.2.2",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.2.2",
|
"@babel/plugin-proposal-class-properties": "^7.2.2",
|
||||||
"@babel/plugin-proposal-decorators": "^7.2.2",
|
"@babel/plugin-proposal-decorators": "^7.2.2",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||||
"@babel/preset-env": "^7.2.2",
|
"@babel/preset-env": "^7.2.2",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import DomainCheck from "./services/DomainCheck"
|
import { parseServices, createMatcher } from 'utils/urlMatcher'
|
||||||
import config from "./config"
|
import remoteServices from "./remoteServices"
|
||||||
|
|
||||||
const domainCheck = new DomainCheck(config)
|
const services = parseServices(remoteServices)
|
||||||
|
const matcher = createMatcher(services)
|
||||||
const { version } = chrome.runtime.getManifest()
|
const { version } = chrome.runtime.getManifest()
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
@@ -10,14 +11,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = domainCheck.match(tab.url)
|
const service = matcher(tab.url)
|
||||||
|
|
||||||
if (service) {
|
if (service) {
|
||||||
chrome.storage.sync.get(
|
chrome.storage.sync.get(
|
||||||
["subdomain", "apiKey"],
|
["subdomain", "apiKey"],
|
||||||
({ subdomain, apiKey }) => {
|
({ subdomain, apiKey }) => {
|
||||||
const settings = { subdomain, apiKey, version }
|
const settings = { subdomain, apiKey, version }
|
||||||
const payload = { service, settings }
|
const payload = { serviceKey: service.key, settings }
|
||||||
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
|
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
|
||||||
console.log("bubble mounted")
|
console.log("bubble mounted")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import { findLastProject, findLastTask, groupedProjectOptions } from "utils"
|
|||||||
@observer
|
@observer
|
||||||
class Bubble extends Component {
|
class Bubble extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
service: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string,
|
||||||
|
projectId: PropTypes.string,
|
||||||
|
taskId: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
settings: PropTypes.shape({
|
settings: PropTypes.shape({
|
||||||
subdomain: PropTypes.string,
|
subdomain: PropTypes.string,
|
||||||
apiKey: PropTypes.string,
|
apiKey: PropTypes.string,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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()}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { createElement } from "react"
|
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 { createEnhancer } from "utils/urlMatcher"
|
||||||
import "../css/main.scss"
|
import "../css/main.scss"
|
||||||
|
|
||||||
|
const serviceEnhancer = createEnhancer(window.document)(services)
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "mountBubble": {
|
case "mountBubble": {
|
||||||
@@ -15,20 +19,24 @@ chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mountBubble = ({ service, settings }) => {
|
const mountBubble = ({ serviceKey, settings }) => {
|
||||||
if (document.getElementById("moco-bx-bubble")) {
|
if (!document.getElementById("moco-bx-container")) {
|
||||||
return
|
const domContainer = document.createElement("div")
|
||||||
|
domContainer.setAttribute("id", "moco-bx-container")
|
||||||
|
document.body.appendChild(domContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
const domContainer = document.createElement("div")
|
if (!document.getElementById("moco-bx-bubble")) {
|
||||||
domContainer.setAttribute("id", "moco-bx-container")
|
const domBubble = document.createElement("div")
|
||||||
document.body.appendChild(domContainer)
|
domBubble.setAttribute("id", "moco-bx-bubble")
|
||||||
|
document.body.appendChild(domBubble)
|
||||||
|
}
|
||||||
|
|
||||||
const domBubble = document.createElement("div")
|
const service = serviceEnhancer(serviceKey, window.location.href)
|
||||||
domBubble.setAttribute("id", "moco-bx-bubble")
|
ReactDOM.render(
|
||||||
document.body.appendChild(domBubble)
|
createElement(Bubble, { service, settings }),
|
||||||
|
document.getElementById("moco-bx-bubble")
|
||||||
ReactDOM.render(createElement(Bubble, { service, settings }), domBubble)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unmountBubble = () => {
|
const unmountBubble = () => {
|
||||||
|
|||||||
30
src/js/remoteServices.js
Normal file
30
src/js/remoteServices.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export default {
|
||||||
|
"github-pr": {
|
||||||
|
name: "github",
|
||||||
|
urlPattern: "https://github.com/:org/:repo/pull/:id(/:tab)",
|
||||||
|
id: (document, service, { org, repo, id }) =>
|
||||||
|
[org, repo, service.key, id].join("."),
|
||||||
|
description: (document, service, { org, repo, id }) =>
|
||||||
|
`${org}/${repo}/${id} - ${document
|
||||||
|
.querySelector(".js-issue-title")
|
||||||
|
?.textContent?.trim()}`,
|
||||||
|
projectId: document => {
|
||||||
|
const match = document
|
||||||
|
.querySelector(".js-issue-title")
|
||||||
|
?.textContent.trim()
|
||||||
|
?.match(/^\[(\d+)\]/)
|
||||||
|
return match && match[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"github-issue": {
|
||||||
|
name: "github",
|
||||||
|
urlPattern: "https://github.com/:org/:repo/issues/:id",
|
||||||
|
id: (document, service, { org, repo, id }) =>
|
||||||
|
[org, repo, "issue", id].join("."),
|
||||||
|
description: (document, service, { org, repo, id }) =>
|
||||||
|
`${org}/${repo}/${id} - ${document
|
||||||
|
.querySelector(".gh-header-title")
|
||||||
|
.textContent.trim()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import Route from "route-parser"
|
|
||||||
|
|
||||||
class DomainCheck {
|
|
||||||
#services;
|
|
||||||
|
|
||||||
constructor(config) {
|
|
||||||
this.#services = config.services.map(service => ({
|
|
||||||
...service,
|
|
||||||
route: new Route(service.urlPattern),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -14,13 +14,13 @@ const nilToArray = input => input || []
|
|||||||
|
|
||||||
export const findLastProject = id =>
|
export const findLastProject = id =>
|
||||||
compose(
|
compose(
|
||||||
find(pathEq("value", id)),
|
find(pathEq("value", Number(id))),
|
||||||
flatMap(get("options"))
|
flatMap(get("options"))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const findLastTask = id =>
|
export const findLastTask = id =>
|
||||||
compose(
|
compose(
|
||||||
find(pathEq("value", id)),
|
find(pathEq("value", Number(id))),
|
||||||
get("tasks")
|
get("tasks")
|
||||||
)
|
)
|
||||||
|
|
||||||
45
src/js/utils/urlMatcher.js
Normal file
45
src/js/utils/urlMatcher.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Route from "route-parser"
|
||||||
|
import { isFunction, isUndefined, compose, toPairs, map } from "lodash/fp"
|
||||||
|
|
||||||
|
const createEvaluator = args => fnOrValue => {
|
||||||
|
if (isUndefined(fnOrValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(fnOrValue)) {
|
||||||
|
return fnOrValue(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fnOrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseServices = compose(
|
||||||
|
map(([key, config]) => ({
|
||||||
|
...config,
|
||||||
|
key,
|
||||||
|
route: new Route(config.urlPattern)
|
||||||
|
})),
|
||||||
|
toPairs
|
||||||
|
)
|
||||||
|
|
||||||
|
export const createEnhancer = document => services => (key, url) => {
|
||||||
|
const service = services[key]
|
||||||
|
service.key = key
|
||||||
|
const route = new Route(service.urlPattern)
|
||||||
|
const match = route.match(url)
|
||||||
|
const args = [document, service, match]
|
||||||
|
const evaluate = createEvaluator(args)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...service,
|
||||||
|
key,
|
||||||
|
url,
|
||||||
|
id: evaluate(service.id),
|
||||||
|
description: evaluate(service.description),
|
||||||
|
projectId: evaluate(service.projectId),
|
||||||
|
taskId: evaluate(service.taskId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMatcher = services => url =>
|
||||||
|
services.find(service => service.route.match(url))
|
||||||
16
test/data.js
16
test/data.js
@@ -1,3 +1,19 @@
|
|||||||
|
export const remoteServices = {
|
||||||
|
'github-pr': {
|
||||||
|
name: 'github',
|
||||||
|
urlPattern: 'https://github.com/:org/:repo/pull/:id',
|
||||||
|
id: (document, service, { org, repo, id }) => [org, repo, service.key, id].join('-'),
|
||||||
|
description: 'This is always the same text',
|
||||||
|
projectId: (document) => {
|
||||||
|
const match = document.querySelector(".gh-header-title").textContent.trim().match(/\[(\d+)\]/)
|
||||||
|
return match && match[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'jira-cloud': {
|
||||||
|
name: 'jira',
|
||||||
|
urlPattern: 'https://cloud.jira.com/browse?project=:project&issue=:id'
|
||||||
|
}
|
||||||
|
}
|
||||||
export const projects = [
|
export const projects = [
|
||||||
{
|
{
|
||||||
id: 944868981,
|
id: 944868981,
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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,9 +1,9 @@
|
|||||||
import { projects } from "./data"
|
import { projects } from "../data"
|
||||||
import {
|
import {
|
||||||
findLastProject,
|
findLastProject,
|
||||||
findLastTask,
|
findLastTask,
|
||||||
groupedProjectOptions
|
groupedProjectOptions
|
||||||
} from "../src/js/utils"
|
} from "../../src/js/utils"
|
||||||
import { map } from "lodash/fp"
|
import { map } from "lodash/fp"
|
||||||
|
|
||||||
describe("utils", () => {
|
describe("utils", () => {
|
||||||
64
test/utils/urlMatcher.test.js
Normal file
64
test/utils/urlMatcher.test.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { remoteServices } from '../data'
|
||||||
|
import { parseServices, createMatcher, createEnhancer } from '../../src/js/utils/urlMatcher'
|
||||||
|
import Route from "route-parser"
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
describe("urlMatcher", () => {
|
||||||
|
describe("parseServices", () => {
|
||||||
|
it("parses the services", () => {
|
||||||
|
const services = parseServices(remoteServices)
|
||||||
|
|
||||||
|
let service = services[0]
|
||||||
|
expect(service.key).toEqual("github-pr")
|
||||||
|
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", () => {
|
||||||
|
let services, matcher
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
services = parseServices(remoteServices)
|
||||||
|
matcher = createMatcher(services)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches host and path', () => {
|
||||||
|
const service = matcher('https://github.com/hundertzehn/mocoapp/pull/123')
|
||||||
|
expect(service.key).toEqual('github-pr')
|
||||||
|
expect(service.name).toEqual('github')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches query string', () => {
|
||||||
|
const service = matcher('https://cloud.jira.com/browse?project=mocoapp&issue=1234')
|
||||||
|
expect(service.key).toEqual('jira-cloud')
|
||||||
|
expect(service.name).toEqual('jira')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not match different host', () => {
|
||||||
|
const service = matcher('https://trello.com/hundertzehn/mocoapp/pull/123')
|
||||||
|
expect(service).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createEnhancer", () => {
|
||||||
|
it("enhances a services", () => {
|
||||||
|
const url = 'https://github.com/hundertzehn/mocoapp/pull/123'
|
||||||
|
const document = {
|
||||||
|
querySelector: jest.fn().mockReturnValue({ textContent: '[4321] Foo' })
|
||||||
|
}
|
||||||
|
const enhancedService = createEnhancer(document)(remoteServices)('github-pr', url)
|
||||||
|
expect(enhancedService.id).toEqual( 'hundertzehn-mocoapp-github-pr-123')
|
||||||
|
expect(enhancedService.description).toEqual('This is always the same text')
|
||||||
|
expect(enhancedService.projectId).toEqual('4321')
|
||||||
|
expect(enhancedService.taskId).toBe(undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
15
yarn.lock
15
yarn.lock
@@ -287,6 +287,14 @@
|
|||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
||||||
|
|
||||||
|
"@babel/plugin-proposal-optional-chaining@^7.2.0":
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.2.0.tgz#ae454f4c21c6c2ce8cb2397dc332ae8b420c5441"
|
||||||
|
integrity sha512-ea3Q6edZC/55wEBVZAEz42v528VulyO0eir+7uky/sT4XRcdkWJcFi1aPtitTlwUzGnECWJNExWww1SStt+yWw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
"@babel/plugin-syntax-optional-chaining" "^7.2.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-unicode-property-regex@^7.2.0":
|
"@babel/plugin-proposal-unicode-property-regex@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520"
|
||||||
@@ -338,6 +346,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-optional-chaining@^7.2.0":
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz#a59d6ae8c167e7608eaa443fda9fa8fa6bf21dff"
|
||||||
|
integrity sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-arrow-functions@^7.2.0":
|
"@babel/plugin-transform-arrow-functions@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
|
||||||
|
|||||||
Reference in New Issue
Block a user