From 6bfd4c2e6fe00d7daa5bfe173bdbaac174b62418 Mon Sep 17 00:00:00 2001 From: Tobias Jacksteit Date: Fri, 29 May 2020 23:43:34 +0800 Subject: [PATCH] configurable host overrides --- src/js/components/Options.js | 35 ++++++++++++++++++-- src/js/content.js | 15 ++++++--- src/js/remoteServices.js | 58 +++++++++++++++++++++++---------- src/js/utils/browser.js | 5 ++- src/js/utils/messageHandlers.js | 7 +++- src/js/utils/settings.js | 17 ++++++++++ src/js/utils/urlMatcher.js | 55 +++++++++++++++++++++++++++---- 7 files changed, 158 insertions(+), 34 deletions(-) create mode 100644 src/js/utils/settings.js diff --git a/src/js/components/Options.js b/src/js/components/Options.js index c54af85..6f95a18 100644 --- a/src/js/components/Options.js +++ b/src/js/components/Options.js @@ -3,18 +3,23 @@ import { observable } from "mobx" import { observer } from "mobx-react" import { isChrome, getSettings, setStorage } from "utils/browser" import ApiClient from "api/Client" +import remoteServices from "../remoteServices"; +import { map } from "lodash" +import { getHostOverridesFromSettings } from "../utils/settings" @observer class Options extends Component { @observable subdomain = "" @observable apiKey = "" + @observable hostOverrides = {} @observable errorMessage = null @observable isSuccess = false componentDidMount() { - getSettings(false).then(({ subdomain, apiKey }) => { - this.subdomain = subdomain || "" - this.apiKey = apiKey || "" + getSettings(false).then((storeData) => { + this.subdomain = storeData.subdomain || "" + this.apiKey = storeData.apiKey || "" + this.hostOverrides = getHostOverridesFromSettings(storeData) || {} }) } @@ -22,13 +27,19 @@ class Options extends Component { this[event.target.name] = event.target.value.trim() } + onChangeHostOverrides = event => { + this.hostOverrides[event.target.name] = event.target.value.trim() + } + handleSubmit = _event => { this.isSuccess = false this.errorMessage = null + setStorage({ subdomain: this.subdomain, apiKey: this.apiKey, settingTimeTrackingHHMM: false, + ...this.hostOverrides, }).then(() => { const { version } = chrome.runtime.getManifest() const apiClient = new ApiClient({ @@ -93,6 +104,24 @@ class Options extends Component { Den API-Schlüssel findest du in deinem Profil unter "Integrationen".

+
+ {map( + remoteServices, + (remoteService, remoteServiceKey) => + remoteService.allowHostOverride && ( +
+ + +
+ ), + )} diff --git a/src/js/content.js b/src/js/content.js index 532c3db..17c8b47 100644 --- a/src/js/content.js +++ b/src/js/content.js @@ -7,11 +7,18 @@ import { createServiceFinder } from "utils/urlMatcher" import remoteServices from "./remoteServices" import { ContentMessenger } from "utils/messaging" import "../css/content.scss" +import { getSettings } from "./utils/browser" +import { getHostOverridesFromSettings } from "./utils/settings" const popupRef = createRef() -const findService = createServiceFinder(remoteServices)(document) -chrome.runtime.onConnect.addListener(function(port) { +let findService +getSettings().then((settings) => { + const hostOverrides = getHostOverridesFromSettings(settings, true) + findService = createServiceFinder(remoteServices, hostOverrides)(document) +}) + +chrome.runtime.onConnect.addListener(function (port) { const messenger = new ContentMessenger(port) function clickHandler(event) { @@ -42,10 +49,10 @@ chrome.runtime.onConnect.addListener(function(port) { leave={{ opacity: "0" }} config={config.stiff} > - {service => + {(service) => service && // eslint-disable-next-line react/display-name - (props => ( + ((props) => ( document => export default { asana: { name: "asana", + host: "https://app.asana.com", urlPatterns: [ - [/^https:\/\/app.asana.com\/0\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], - [/^https:\/\/app.asana.com\/0\/search\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], + [/^__HOST__\/0\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], + [/^__HOST__\/0\/search\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], ], description: document => document.querySelector(".ItemRow--highlighted textarea")?.textContent?.trim() || @@ -19,32 +20,38 @@ export default { document.querySelector(".SingleTaskPane textarea")?.textContent?.trim() || document.querySelector(".SingleTaskTitleInput-taskName textarea")?.textContent?.trim(), projectId: projectIdentifierBySelector(".TopbarPageHeaderStructure-titleRow h1"), + allowHostOverride: false, }, "github-pr": { name: "github", - urlPatterns: ["https\\://github.com/:org/:repo/pull/:id(/:tab)"], + host: "https://github.com", + urlPatterns: ["__HOST__/:org/:repo/pull/:id(/:tab)"], id: (document, service, { org, repo, id }) => [service.key, org, repo, id].join("."), description: document => document.querySelector(".js-issue-title")?.textContent?.trim(), projectId: projectIdentifierBySelector(".js-issue-title"), + allowHostOverride: true, }, "github-issue": { name: "github", - urlPatterns: ["https\\://github.com/:org/:repo/issues/:id"], + host: "https://github.com", + urlPatterns: ["__HOST__/:org/:repo/issues/:id"], id: (document, service, { org, repo, id }) => [service.key, org, repo, id].join("."), description: (document, service, { org, repo, id }) => document.querySelector(".js-issue-title")?.textContent?.trim(), projectId: projectIdentifierBySelector(".js-issue-title"), + allowHostOverride: true, }, jira: { name: "jira", + host: "https://:org.atlassian.net", urlPatterns: [ - "https\\://:org.atlassian.net/secure/RapidBoard.jspa", - "https\\://:org.atlassian.net/browse/:id", - "https\\://:org.atlassian.net/jira/software/projects/:projectId/boards/:board", - "https\\://:org.atlassian.net/jira/software/projects/:projectId/boards/:board/backlog", + "__HOST__/secure/RapidBoard.jspa", + "__HOST__/browse/:id", + "__HOST__/jira/software/projects/:projectId/boards/:board", + "__HOST__/jira/software/projects/:projectId/boards/:board/backlog", ], queryParams: { id: "selectedIssue", @@ -59,11 +66,13 @@ export default { document.querySelector(".ghx-selected .ghx-summary")?.textContent?.trim() return `#${id} ${title || ""}` }, + allowHostOverride: true, }, meistertask: { name: "meistertask", - urlPatterns: ["https\\://www.meistertask.com/app/task/:id/:slug"], + host: "https://www.meistertask.com", + urlPatterns: ["/app/task/:id/:slug"], description: document => { const json = document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}" const data = JSON.parse(json) @@ -75,30 +84,36 @@ export default { const match = data.taskName?.match(projectRegex) || data.projectName?.match(projectRegex) return match && match[1] }, + allowHostOverride: false, }, trello: { name: "trello", - urlPatterns: ["https\\://trello.com/c/:id/:title"], + host: "https://trello.com", + urlPatterns: ["__HOST__/c/:id/:title"], description: (document, service, { title }) => document.querySelector(".js-title-helper")?.textContent?.trim() || title, projectId: document => projectIdentifierBySelector(".js-title-helper")(document) || projectIdentifierBySelector(".js-board-editing-target")(document), + allowHostOverride: false, }, youtrack: { name: "youtrack", - urlPatterns: ["https\\://:org.myjetbrains.com/youtrack/issue/:id"], + host: "https://:org.myjetbrains.com", + urlPatterns: ["__HOST__/youtrack/issue/:id"], description: document => document.querySelector("yt-issue-body h1")?.textContent?.trim(), projectId: projectIdentifierBySelector("yt-issue-body h1"), + allowHostOverride: true, }, wrike: { name: "wrike", + host: "https://www.wrike.com", urlPatterns: [ - "https\\://www.wrike.com/workspace.htm#path=mywork", - "https\\://www.wrike.com/workspace.htm#path=folder", + "__HOST__/workspace.htm#path=mywork", + "__HOST__/workspace.htm#path=folder", "https\\://app-eu.wrike.com/workspace.htm#path=mywork", "https\\://app-eu.wrike.com/workspace.htm#path=folder", ], @@ -107,39 +122,46 @@ export default { }, description: document => document.querySelector(".title-field-ghost")?.textContent?.trim(), projectId: projectIdentifierBySelector(".header-title__main"), + allowHostOverride: false, }, wunderlist: { name: "wunderlist", - urlPatterns: ["https\\://www.wunderlist.com/(webapp)#/tasks/:id(/*)"], + host: "https://www.wunderlist.com", + urlPatterns: ["__HOST__/(webapp)#/tasks/:id(/*)"], description: document => document .querySelector(".taskItem.selected .taskItem-titleWrapper-title") ?.textContent?.trim(), projectId: projectIdentifierBySelector(".taskItem.selected .taskItem-titleWrapper-title"), + allowHostOverride: false, }, "gitlab-mr": { name: "gitlab", + host: "https://gitlab.com", urlPatterns: [ - "https\\://gitlab.com/:org/:group/:projectId/-/merge_requests/:id", - "https\\://gitlab.com/:org/:projectId/-/merge_requests/:id", + "__HOST__/:org/:group/:projectId/-/merge_requests/:id", + "__HOST__/:org/:projectId/-/merge_requests/:id", ], description: (document, service, { id }) => { const title = document.querySelector("h2.title")?.textContent?.trim() return `#${id} ${title || ""}`.trim() }, + allowHostOverride: true, }, "gitlab-issues": { name: "gitlab", + host: "https://gitlab.com", urlPatterns: [ - "https\\://gitlab.com/:org/:group/:projectId/-/issues/:id", - "https\\://gitlab.com/:org/:projectId/-/issues/:id", + "__HOST__/:org/:group/:projectId/-/issues/:id", + "__HOST__/:org/:projectId/-/issues/:id", ], description: (document, service, { id }) => { const title = document.querySelector("h2.title")?.textContent?.trim() return `#${id} ${title || ""}`.trim() }, + allowHostOverride: true, }, } diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js index 985fbea..c6da9c7 100644 --- a/src/js/utils/browser.js +++ b/src/js/utils/browser.js @@ -1,11 +1,14 @@ import { head } from "lodash/fp" +import remoteServices from "../remoteServices" + export const isChrome = () => typeof browser === "undefined" && chrome export const isFirefox = () => typeof browser !== "undefined" && chrome const DEFAULT_SUBDOMAIN = "unset" export const getSettings = (withDefaultSubdomain = true) => { - const keys = ["subdomain", "apiKey", "settingTimeTrackingHHMM"] + const keys = ["subdomain", "apiKey", "settingTimeTrackingHHMM", + ...Object.keys(remoteServices).map(key => `hostOverrides:${key}`)] const { version } = chrome.runtime.getManifest() if (isChrome()) { return new Promise(resolve => { diff --git a/src/js/utils/messageHandlers.js b/src/js/utils/messageHandlers.js index 705de4b..6f3615c 100644 --- a/src/js/utils/messageHandlers.js +++ b/src/js/utils/messageHandlers.js @@ -11,8 +11,13 @@ import { get, forEach, reject, isNil } from "lodash/fp" import { createMatcher } from "utils/urlMatcher" import remoteServices from "remoteServices" import { queryTabs, isBrowserTab, getSettings, setStorage } from "utils/browser" +import { getHostOverridesFromSettings } from "./settings" -const matcher = createMatcher(remoteServices) +let matcher +getSettings().then((settings) => { + const hostOverrides = getHostOverridesFromSettings(settings, true) + matcher = createMatcher(remoteServices, hostOverrides) +}) export function tabUpdated(tab, { messenger, settings }) { messenger.connectTab(tab) diff --git a/src/js/utils/settings.js b/src/js/utils/settings.js new file mode 100644 index 0000000..5cbc464 --- /dev/null +++ b/src/js/utils/settings.js @@ -0,0 +1,17 @@ +import { filter, fromPairs, map, toPairs } from "lodash" + +export const getHostOverridesFromSettings = (settings, removePrefix) => { + return fromPairs( + map( + filter(toPairs(settings), (item) => { + return item[0].indexOf("hostOverrides") !== -1 + }), + (item) => { + if (removePrefix) { + item[0] = item[0].replace("hostOverrides:", "") + } + return item + }, + ), + ) +} diff --git a/src/js/utils/urlMatcher.js b/src/js/utils/urlMatcher.js index 4767fa2..47aa6c6 100644 --- a/src/js/utils/urlMatcher.js +++ b/src/js/utils/urlMatcher.js @@ -1,5 +1,5 @@ import UrlPattern from "url-pattern" -import { isFunction, isUndefined, compose, toPairs, map, pipe, isNil } from "lodash/fp" +import { isFunction, isUndefined, compose, toPairs, map, pipe, isNil, convert } from "lodash/fp" import { asArray } from "./index" import queryString from "query-string" @@ -39,15 +39,36 @@ const createEvaluator = args => fnOrValue => { return fnOrValue } +const prepareHostForRegExp = (host) => { + if (isUndefined(host)) { + return + } + + return host.replace(":", "\\:")//.replace(/\//g, "\\/") +} + +const replaceHostInPattern = (host, pattern) => { + if(typeof pattern === "string") { + return pattern.replace("__HOST__", prepareHostForRegExp(host)) + } else if(pattern instanceof RegExp) { + return new RegExp(pattern.source.replace("__HOST__", prepareHostForRegExp(host))) + } else { + console.error("Invalid type for pattern %v, no host replacement performed", pattern) + return pattern + } +} + const parseServices = compose( map(([key, config]) => ({ ...config, key, patterns: config.urlPatterns.map(pattern => { if (Array.isArray(pattern)) { - return new UrlPattern(...pattern) + return new UrlPattern( + ...pattern.map((p, index) => (index === 0 ? replaceHostInPattern(config.host, p) : p)), + ) } - return new UrlPattern(pattern) + return new UrlPattern(replaceHostInPattern(config.host, pattern)) }), })), toPairs, @@ -72,8 +93,28 @@ export const createEnhancer = document => service => { } } -export const createMatcher = remoteServices => { - const services = parseServices(remoteServices) +const applyHostOverrides = (remoteServices, hostOverrides) => { + let appliedRemoteServices = Object.assign(remoteServices) + if (isUndefined(hostOverrides)) { + console.error("No overrides found.") + return remoteServices + } + + Object.keys(remoteServices).forEach((key) => { + const remoteService = remoteServices[key]; + appliedRemoteServices[key] = { + ...remoteService, + key, + host: (hostOverrides && hostOverrides[key]) || remoteService.host, + } + }) + + return appliedRemoteServices +} + +export const createMatcher = (remoteServices, hostOverrides) => { + const services = parseServices(applyHostOverrides(remoteServices, hostOverrides)) + return tabUrl => { const { origin, pathname, hash, query } = parseUrl(tabUrl) const url = `${origin}${pathname}${hash}` @@ -99,8 +140,8 @@ export const createMatcher = remoteServices => { } } -export const createServiceFinder = remoteServices => document => { - const matcher = createMatcher(remoteServices) +export const createServiceFinder = (remoteServices, hostOverrides) => document => { + const matcher = createMatcher(remoteServices, hostOverrides) const enhancer = createEnhancer(document) return pipe( matcher,