From 1cbf5b57029c4a7d1c63e68660fc81f7169aa0e6 Mon Sep 17 00:00:00 2001 From: Manuel Bouza Date: Mon, 15 Jun 2020 10:52:02 +0200 Subject: [PATCH] Refactor --- src/css/options.scss | 11 +++++- src/js/components/App.js | 12 +++--- src/js/components/Options.js | 53 ++++++++++++++------------ src/js/options.js | 1 + src/js/popup.js | 1 + src/js/remoteServices.js | 40 ++++++++++---------- src/js/utils/browser.js | 42 +++++++++++++-------- src/js/utils/index.js | 43 ++++++--------------- src/js/utils/urlMatcher.js | 72 +++++++++++++++--------------------- 9 files changed, 134 insertions(+), 141 deletions(-) diff --git a/src/css/options.scss b/src/css/options.scss index 28091ba..098a5d3 100644 --- a/src/css/options.scss +++ b/src/css/options.scss @@ -11,6 +11,11 @@ .moco-bx-options { padding: 0rem 2rem 2rem; + a { + color: $blue; + text-decoration: none; + } + p { margin: 0.5rem 0; } @@ -45,9 +50,13 @@ } &__host-overrides { - padding-bottom: 1rem; + margin-bottom: 1.5rem; text-align: center; font-weight: normal; } + + small { + font-size: 0.8rem; + } } } diff --git a/src/js/components/App.js b/src/js/components/App.js index 520e206..3ff6013 100644 --- a/src/js/components/App.js +++ b/src/js/components/App.js @@ -119,7 +119,7 @@ class App extends Component { chrome.runtime.onMessage.removeListener(this.handleSetFormErrors) } - handleChange = event => { + handleChange = (event) => { const { projects } = this.props const { target: { name, value }, @@ -133,11 +133,11 @@ class App extends Component { } } - handleSelectDate = date => { + handleSelectDate = (date) => { this.changeset.date = formatDate(date) } - handleStopTimer = timedActivity => { + handleStopTimer = (timedActivity) => { const { service } = this.props chrome.runtime.sendMessage({ @@ -146,7 +146,7 @@ class App extends Component { }) } - handleSubmit = event => { + handleSubmit = (event) => { event.preventDefault() const { service } = this.props @@ -159,7 +159,7 @@ class App extends Component { }) } - handleKeyDown = event => { + handleKeyDown = (event) => { if (event.keyCode === 27) { event.stopPropagation() chrome.runtime.sendMessage({ type: "closePopup" }) @@ -204,7 +204,7 @@ class App extends Component { return ( - {props => ( + {(props) => (
diff --git a/src/js/components/Options.js b/src/js/components/Options.js index 0f8e41b..cf785e3 100644 --- a/src/js/components/Options.js +++ b/src/js/components/Options.js @@ -3,8 +3,7 @@ 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 { pipe, toPairs, fromPairs, prop, map, sortedUniqBy, filter } from "lodash/fp" +import { pipe, toPairs, fromPairs, map } from "lodash/fp" function upperCaseFirstLetter(input) { return input[0].toUpperCase() + input.slice(1) @@ -14,15 +13,6 @@ function removePathFromUrl(url) { return url.replace(/(\.[a-z]+)\/.*$/, "$1") } -const overridableRemoveServices = pipe( - filter(prop("allowHostOverride")), - map((remoteService) => ({ - name: remoteService.name, - host: remoteService.host, - })), - sortedUniqBy("name"), -)(remoteServices) - @observer class Options extends Component { @observable subdomain = "" @@ -33,10 +23,10 @@ class Options extends Component { @observable showHostOverrideOptions = false componentDidMount() { - getSettings(false).then((storeData) => { - this.subdomain = storeData.subdomain || "" - this.apiKey = storeData.apiKey || "" - this.hostOverrides = storeData.hostOverrides + getSettings(false).then((settings) => { + this.subdomain = settings.subdomain || "" + this.apiKey = settings.apiKey || "" + this.hostOverrides = settings.hostOverrides }) } @@ -132,27 +122,42 @@ class Options extends Component { {!this.showHostOverrideOptions && ( )} {this.showHostOverrideOptions && ( -
-

URLs für Dienste

- {overridableRemoveServices.map((remoteService) => ( -
+
+

Service-URLs

+ + Doppelpunkt für Platzhalter verwenden, z.B.{" "} + :org. Siehe{" "} + + Online-Doku + + . + +
+ {pipe( + Object.entries, + Array.from, + )(this.hostOverrides).map(([name, host]) => ( +
- {upperCaseFirstLetter(remoteService.name)} + {upperCaseFirstLetter(name)} diff --git a/src/js/options.js b/src/js/options.js index 20f7f93..bdd36bf 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -1,3 +1,4 @@ +import "mobx-react-lite/batchingForReactDom" import React from "react" import ReactDOM from "react-dom" import Options from "./components/Options" diff --git a/src/js/popup.js b/src/js/popup.js index d308e9c..77cec7f 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -1,3 +1,4 @@ +import "mobx-react-lite/batchingForReactDom" import React from "react" import ReactDOM from "react-dom" import App from "./components/App" diff --git a/src/js/remoteServices.js b/src/js/remoteServices.js index 5d96906..b87a9a9 100644 --- a/src/js/remoteServices.js +++ b/src/js/remoteServices.js @@ -8,8 +8,8 @@ export default { name: "asana", host: "https://app.asana.com", urlPatterns: [ - [/^__HOST__\/0\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], - [/^__HOST__\/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() || @@ -23,32 +23,32 @@ export default { "github-pr": { name: "github", host: "https://github.com", - urlPatterns: ["__HOST__/:org/:repo/pull/:id(/:tab)"], + 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, + allowHostOverride: false, }, "github-issue": { name: "github", host: "https://github.com", - urlPatterns: ["__HOST__/:org/:repo/issues/:id"], + 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, + allowHostOverride: false, }, jira: { name: "jira", host: "https://:org.atlassian.net", urlPatterns: [ - "__HOST__/secure/RapidBoard.jspa", - "__HOST__/browse/:id", - "__HOST__/jira/software/projects/:projectId/boards/:board", - "__HOST__/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", @@ -69,7 +69,7 @@ export default { meistertask: { name: "meistertask", host: "https://www.meistertask.com", - urlPatterns: ["/app/task/:id/:slug"], + urlPatterns: [":host:/app/task/:id/:slug"], description: (document) => { const json = document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}" const data = JSON.parse(json) @@ -87,7 +87,7 @@ export default { trello: { name: "trello", host: "https://trello.com", - urlPatterns: ["__HOST__/c/:id/:title"], + urlPatterns: [":host:/c/:id/:title"], description: (document, service, { title }) => document.querySelector(".js-title-helper")?.textContent?.trim() || title, projectId: (document) => @@ -99,7 +99,7 @@ export default { youtrack: { name: "youtrack", host: "https://:org.myjetbrains.com", - urlPatterns: ["__HOST__/youtrack/issue/:id"], + urlPatterns: [":host:/youtrack/issue/:id"], description: (document) => document.querySelector("yt-issue-body h1")?.textContent?.trim(), projectId: projectIdentifierBySelector("yt-issue-body h1"), allowHostOverride: true, @@ -109,8 +109,8 @@ export default { name: "wrike", host: "https://www.wrike.com", urlPatterns: [ - "__HOST__/workspace.htm#path=mywork", - "__HOST__/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", ], @@ -125,7 +125,7 @@ export default { wunderlist: { name: "wunderlist", host: "https://www.wunderlist.com", - urlPatterns: ["__HOST__/(webapp)#/tasks/:id(/*)"], + urlPatterns: [":host:/(webapp)#/tasks/:id(/*)"], description: (document) => document .querySelector(".taskItem.selected .taskItem-titleWrapper-title") @@ -138,8 +138,8 @@ export default { name: "gitlab", host: "https://gitlab.com", urlPatterns: [ - "__HOST__/:org/:group/:projectId/-/merge_requests/:id", - "__HOST__/: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() @@ -152,8 +152,8 @@ export default { name: "gitlab", host: "https://gitlab.com", urlPatterns: [ - "__HOST__/:org/:group/:projectId/-/issues/:id", - "__HOST__/: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() diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js index 721029d..f8ab572 100644 --- a/src/js/utils/browser.js +++ b/src/js/utils/browser.js @@ -1,36 +1,46 @@ -import { head } from "lodash/fp" +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 -const DEFAULT_SUBDOMAIN = "unset" -const DEFAULT_HOST_OVERRIDES = { - github: "https://github.com", - gitlab: "https://gitlab.com", - jira: "https://:org.atlassian.net", - youtrack: "https://:org.myjetbrains.com", -} +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", "hostOverrides"] const { version } = chrome.runtime.getManifest() if (isChrome()) { return new Promise((resolve) => { - chrome.storage.sync.get(keys, (data) => { + chrome.storage.sync.get(keys, (settings) => { if (withDefaultSubdomain) { - data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN + settings.subdomain = settings.subdomain || DEFAULT_SUBDOMAIN } - data.hostOverrides = { ...DEFAULT_HOST_OVERRIDES, ...(data.hostOverrides || {}) } - 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 } - data.hostOverrides = { ...DEFAULT_HOST_OVERRIDES, ...(data.hostOverrides || {}) } - return { ...data, version } + settings.hostOverrides = getHostOverrides(settings) + return { ...settings, version } }) } } diff --git a/src/js/utils/index.js b/src/js/utils/index.js index c207d9d..796e2ea 100644 --- a/src/js/utils/index.js +++ b/src/js/utils/index.js @@ -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) { diff --git a/src/js/utils/urlMatcher.js b/src/js/utils/urlMatcher.js index f4fb98a..9c522f0 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, convert } 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 } @@ -40,20 +40,15 @@ const createEvaluator = args => fnOrValue => { } const prepareHostForRegExp = (host) => { - if (isUndefined(host)) { - return - } - 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))) + 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 } } @@ -62,11 +57,9 @@ 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.map((p, index) => (index === 0 ? replaceHostInPattern(config.host, p) : p)), - ) + return new UrlPattern(...pattern.map((p) => replaceHostInPattern(config.host, p))) } return new UrlPattern(replaceHostInPattern(config.host, pattern)) }), @@ -74,7 +67,7 @@ const parseServices = compose( toPairs, ) -export const createEnhancer = document => service => { +export const createEnhancer = (document) => (service) => { if (!service) { return } @@ -93,38 +86,34 @@ export const createEnhancer = document => service => { } } -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[remoteService.name]) || remoteService.host, - } - }) - - return appliedRemoteServices -} +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 => { + 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) @@ -140,11 +129,8 @@ export const createMatcher = (remoteServices, hostOverrides) => { } } -export const createServiceFinder = (remoteServices, hostOverrides) => document => { +export const createServiceFinder = (remoteServices, hostOverrides) => (document) => { const matcher = createMatcher(remoteServices, hostOverrides) const enhancer = createEnhancer(document) - return pipe( - matcher, - enhancer, - ) + return pipe(matcher, enhancer) }