Enhance service
This commit is contained in:
3
.babelrc
3
.babelrc
@@ -2,6 +2,7 @@
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [
|
||||
["@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/plugin-proposal-class-properties": "^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-react": "^7.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import DomainCheck from "./services/DomainCheck"
|
||||
import config from "./config"
|
||||
import { parseServices, createMatcher } from 'utils/urlMatcher'
|
||||
import remoteServices from "./remoteServices"
|
||||
|
||||
const domainCheck = new DomainCheck(config)
|
||||
const services = parseServices(remoteServices)
|
||||
const matcher = createMatcher(services)
|
||||
const { version } = chrome.runtime.getManifest()
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
@@ -10,14 +11,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
return
|
||||
}
|
||||
|
||||
const service = domainCheck.match(tab.url)
|
||||
const service = matcher(tab.url)
|
||||
|
||||
if (service) {
|
||||
chrome.storage.sync.get(
|
||||
["subdomain", "apiKey"],
|
||||
({ subdomain, apiKey }) => {
|
||||
const settings = { subdomain, apiKey, version }
|
||||
const payload = { service, settings }
|
||||
const payload = { serviceKey: service.key, settings }
|
||||
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
|
||||
console.log("bubble mounted")
|
||||
})
|
||||
|
||||
@@ -12,6 +12,14 @@ import { findLastProject, findLastTask, groupedProjectOptions } from "utils"
|
||||
@observer
|
||||
class Bubble extends Component {
|
||||
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({
|
||||
subdomain: 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 ReactDOM from "react-dom"
|
||||
import Bubble from "./components/Bubble"
|
||||
import services from "remoteServices"
|
||||
import { createEnhancer } from "utils/urlMatcher"
|
||||
import "../css/main.scss"
|
||||
|
||||
const serviceEnhancer = createEnhancer(window.document)(services)
|
||||
|
||||
chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
||||
switch (type) {
|
||||
case "mountBubble": {
|
||||
@@ -15,20 +19,24 @@ chrome.runtime.onMessage.addListener(({ type, payload }) => {
|
||||
}
|
||||
})
|
||||
|
||||
const mountBubble = ({ service, settings }) => {
|
||||
if (document.getElementById("moco-bx-bubble")) {
|
||||
return
|
||||
const mountBubble = ({ serviceKey, settings }) => {
|
||||
if (!document.getElementById("moco-bx-container")) {
|
||||
const domContainer = document.createElement("div")
|
||||
domContainer.setAttribute("id", "moco-bx-container")
|
||||
document.body.appendChild(domContainer)
|
||||
}
|
||||
|
||||
const domContainer = document.createElement("div")
|
||||
domContainer.setAttribute("id", "moco-bx-container")
|
||||
document.body.appendChild(domContainer)
|
||||
if (!document.getElementById("moco-bx-bubble")) {
|
||||
const domBubble = document.createElement("div")
|
||||
domBubble.setAttribute("id", "moco-bx-bubble")
|
||||
document.body.appendChild(domBubble)
|
||||
}
|
||||
|
||||
const domBubble = document.createElement("div")
|
||||
domBubble.setAttribute("id", "moco-bx-bubble")
|
||||
document.body.appendChild(domBubble)
|
||||
|
||||
ReactDOM.render(createElement(Bubble, { service, settings }), domBubble)
|
||||
const service = serviceEnhancer(serviceKey, window.location.href)
|
||||
ReactDOM.render(
|
||||
createElement(Bubble, { service, settings }),
|
||||
document.getElementById("moco-bx-bubble")
|
||||
)
|
||||
}
|
||||
|
||||
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 =>
|
||||
compose(
|
||||
find(pathEq("value", id)),
|
||||
find(pathEq("value", Number(id))),
|
||||
flatMap(get("options"))
|
||||
)
|
||||
|
||||
export const findLastTask = id =>
|
||||
compose(
|
||||
find(pathEq("value", id)),
|
||||
find(pathEq("value", Number(id))),
|
||||
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 = [
|
||||
{
|
||||
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 {
|
||||
findLastProject,
|
||||
findLastTask,
|
||||
groupedProjectOptions
|
||||
} from "../src/js/utils"
|
||||
} from "../../src/js/utils"
|
||||
import { map } from "lodash/fp"
|
||||
|
||||
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/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":
|
||||
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"
|
||||
@@ -338,6 +346,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "7.2.0"
|
||||
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