feature/host-overrides (#161)

* configurable host overrides

* base host overrides on name of service instead of key and hide the options by default

* added unit tests

* review changes

* Refactor options

* Refactor

* Update Readme

* Pump version and update Changelog

Co-authored-by: Tobias Jacksteit <me@xtj7.de>
This commit is contained in:
Manuel Bouza
2020-06-15 17:14:31 +02:00
committed by GitHub
parent a13e30784c
commit 061a3d9a89
19 changed files with 361 additions and 147 deletions

View File

@@ -1,34 +1,53 @@
import { head } from "lodash/fp"
export const isChrome = () => typeof browser === "undefined" && chrome
export const isFirefox = () => typeof browser !== "undefined" && chrome
import { head, pick, reduce, filter, prop, pipe } from "lodash/fp"
import remoteServices from "../remoteServices"
const DEFAULT_SUBDOMAIN = "unset"
export const isChrome = () => typeof browser === "undefined" && chrome
export const isFirefox = () => typeof browser !== "undefined" && chrome
export const defaultHostOverrides = pipe(
filter(prop("allowHostOverride")),
reduce((acc, remoteService) => {
acc[remoteService.name] = remoteService.host
return acc
}, {}),
)(remoteServices)
// We pick only the keys defined in `defaultHostOverrides`, so that
// deleted host overrides get cleared from the settings
const getHostOverrides = (settings) => ({
...defaultHostOverrides,
...pick(Object.keys(defaultHostOverrides), settings.hostOverrides || {}),
})
export const getSettings = (withDefaultSubdomain = true) => {
const keys = ["subdomain", "apiKey", "settingTimeTrackingHHMM"]
const keys = ["subdomain", "apiKey", "settingTimeTrackingHHMM", "hostOverrides"]
const { version } = chrome.runtime.getManifest()
if (isChrome()) {
return new Promise(resolve => {
chrome.storage.sync.get(keys, data => {
return new Promise((resolve) => {
chrome.storage.sync.get(keys, (settings) => {
if (withDefaultSubdomain) {
data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
settings.subdomain = settings.subdomain || DEFAULT_SUBDOMAIN
}
resolve({ ...data, version })
settings.hostOverrides = getHostOverrides(settings)
resolve({ ...settings, version })
})
})
} else {
return browser.storage.sync.get(keys).then(data => {
return browser.storage.sync.get(keys).then((settings) => {
if (withDefaultSubdomain) {
data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
settings.subdomain = settings.subdomain || DEFAULT_SUBDOMAIN
}
return { ...data, version }
settings.hostOverrides = getHostOverrides(settings)
return { ...settings, version }
})
}
}
export const setStorage = items => {
export const setStorage = (items) => {
if (isChrome()) {
return new Promise(resolve => {
return new Promise((resolve) => {
chrome.storage.sync.set(items, resolve)
})
} else {
@@ -36,9 +55,9 @@ export const setStorage = items => {
}
}
export const queryTabs = queryInfo => {
export const queryTabs = (queryInfo) => {
if (isChrome()) {
return new Promise(resolve => chrome.tabs.query(queryInfo, resolve))
return new Promise((resolve) => chrome.tabs.query(queryInfo, resolve))
} else {
return browser.tabs.query(queryInfo)
}
@@ -48,4 +67,4 @@ export const getCurrentTab = () => {
return queryTabs({ currentWindow: true, active: true }).then(head)
}
export const isBrowserTab = tab => /^(?:chrome|about):/.test(tab.url)
export const isBrowserTab = (tab) => /^(?:chrome|about):/.test(tab.url)

View File

@@ -17,41 +17,30 @@ import {
import { startOfWeek, endOfWeek } from "date-fns"
import { format } from "date-fns"
const nilToArray = input => input || []
const nilToArray = (input) => input || []
export const ERROR_UNAUTHORIZED = "unauthorized"
export const ERROR_UPGRADE_REQUIRED = "upgrade-required"
export const ERROR_UNKNOWN = "unknown"
export const noop = () => null
export const asArray = input => (Array.isArray(input) ? input : [input])
export const asArray = (input) => (Array.isArray(input) ? input : [input])
export const findProjectBy = prop => val => projects => {
export const findProjectBy = (prop) => (val) => (projects) => {
if (!val) {
return undefined
}
return compose(
find(pathEq(prop, val)),
flatMap(get("options")),
)(projects)
return compose(find(pathEq(prop, val)), flatMap(get("options")))(projects)
}
export const findProjectByIdentifier = findProjectBy("identifier")
export const findProjectByValue = findProjectBy("value")
export const findTask = id =>
compose(
find(pathEq("value", Number(id))),
get("tasks"),
)
export const findTask = (id) => compose(find(pathEq("value", Number(id))), get("tasks"))
export const defaultTask = tasks =>
compose(
defaultTo(head(tasks)),
find(pathEq("isDefault", true)),
nilToArray,
)(tasks)
export const defaultTask = (tasks) =>
compose(defaultTo(head(tasks)), find(pathEq("isDefault", true)), nilToArray)(tasks)
function taskOptions(tasks) {
return tasks.map(({ id, name, billable, default: isDefault }) => ({
@@ -63,7 +52,7 @@ function taskOptions(tasks) {
}
export function projectOptions(projects) {
return projects.map(project => ({
return projects.map((project) => ({
value: project.id,
label: project.intern ? `(${project.name})` : project.name,
identifier: project.identifier,
@@ -82,17 +71,9 @@ export const groupedProjectOptions = compose(
nilToArray,
)
export const serializeProps = attrs =>
compose(
mapValues(JSON.stringify),
pick(attrs),
)
export const serializeProps = (attrs) => compose(mapValues(JSON.stringify), pick(attrs))
export const parseProps = attrs =>
compose(
mapValues(JSON.parse),
pick(attrs),
)
export const parseProps = (attrs) => compose(mapValues(JSON.parse), pick(attrs))
export const trace = curry((tag, value) => {
// eslint-disable-next-line no-console
@@ -101,13 +82,13 @@ export const trace = curry((tag, value) => {
})
export const weekStartsOn = 1
export const formatDate = date => format(date, "yyyy-MM-dd")
export const formatDate = (date) => format(date, "yyyy-MM-dd")
export const getStartOfWeek = () => startOfWeek(new Date(), { weekStartsOn })
export const getEndOfWeek = () => endOfWeek(new Date(), { weekStartsOn })
export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`
export const extractAndSetTag = changeset => {
export const extractAndSetTag = (changeset) => {
let { description } = changeset
const match = description.match(/^#(\S+)/)
if (!match) {

View File

@@ -12,7 +12,15 @@ import { createMatcher } from "utils/urlMatcher"
import remoteServices from "remoteServices"
import { queryTabs, isBrowserTab, getSettings, setStorage } from "utils/browser"
const matcher = createMatcher(remoteServices)
let matcher
const initMatcher = (settings) => {
matcher = createMatcher(remoteServices, settings.hostOverrides)
}
getSettings().then((settings) => {
initMatcher(settings)
})
export function tabUpdated(tab, { messenger, settings }) {
messenger.connectTab(tab)
@@ -54,6 +62,8 @@ export function tabUpdated(tab, { messenger, settings }) {
}
export function settingsChanged(settings, { messenger }) {
initMatcher(settings)
queryTabs({ currentWindow: true })
.then(reject(isBrowserTab))
.then(

View File

@@ -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, reduce } from "lodash/fp"
import { asArray } from "./index"
import queryString from "query-string"
@@ -19,7 +19,7 @@ function parseUrl(url) {
function extractQueryParams(queryParams, query) {
return toPairs(queryParams).reduce((acc, [key, params]) => {
const param = asArray(params).find(param => !isNil(query[param]))
const param = asArray(params).find((param) => !isNil(query[param]))
if (param) {
acc[key] = query[param]
}
@@ -27,7 +27,7 @@ function extractQueryParams(queryParams, query) {
}, {})
}
const createEvaluator = args => fnOrValue => {
const createEvaluator = (args) => (fnOrValue) => {
if (isUndefined(fnOrValue)) {
return
}
@@ -39,21 +39,35 @@ const createEvaluator = args => fnOrValue => {
return fnOrValue
}
const prepareHostForRegExp = (host) => {
return host.replace(":", "\\:")
}
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 {
return pattern
}
}
const parseServices = compose(
map(([key, config]) => ({
...config,
key,
patterns: config.urlPatterns.map(pattern => {
patterns: config.urlPatterns.map((pattern) => {
if (Array.isArray(pattern)) {
return new UrlPattern(...pattern)
return new UrlPattern(...pattern.map((p) => replaceHostInPattern(config.host, p)))
}
return new UrlPattern(pattern)
return new UrlPattern(replaceHostInPattern(config.host, pattern))
}),
})),
toPairs,
)
export const createEnhancer = document => service => {
export const createEnhancer = (document) => (service) => {
if (!service) {
return
}
@@ -72,18 +86,34 @@ export const createEnhancer = document => service => {
}
}
export const createMatcher = remoteServices => {
const services = parseServices(remoteServices)
return tabUrl => {
const applyHostOverrides = (remoteServices, hostOverrides) =>
pipe(
toPairs,
reduce((acc, [key, remoteService]) => {
acc[key] = {
...remoteService,
key,
host: hostOverrides[remoteService.name] || remoteService.host,
}
return acc
}, {}),
)(remoteServices)
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}`
const service = services.find(service => service.patterns.some(pattern => pattern.match(url)))
const service = services.find((service) => {
return service.patterns.some((pattern) => pattern.match(url))
})
if (!service) {
return
}
const pattern = service.patterns.find(pattern => pattern.match(url))
const pattern = service.patterns.find((pattern) => pattern.match(url))
let match = pattern.match(url)
if (service.queryParams) {
const extractedQueryParams = extractQueryParams(service.queryParams, query)
@@ -99,11 +129,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,
enhancer,
)
return pipe(matcher, enhancer)
}