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:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user