Enhance service

This commit is contained in:
Manuel Bouza
2019-02-07 18:26:15 +01:00
parent 7ad7cab5c0
commit cef97a5829
15 changed files with 210 additions and 112 deletions

View File

@@ -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"]
]
}

View File

@@ -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",

View File

@@ -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")
})

View File

@@ -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,

View File

@@ -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()}`
}
]
}

View File

@@ -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
View 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()}`
}
}

View File

@@ -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

View File

@@ -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")
)

View 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))

View File

@@ -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,

View File

@@ -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()
})
})
})

View File

@@ -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", () => {

View 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)
})
})
})
})

View File

@@ -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"