diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8351c19 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14 diff --git a/src/css/_button.scss b/src/css/_button.scss index c96220b..65d2956 100644 --- a/src/css/_button.scss +++ b/src/css/_button.scss @@ -48,3 +48,17 @@ button.moco-bx-btn { margin-left: 0.5rem; } } + +.moco-bx-btn__secondary { + color: $blue; + border: none; + background: none; + text-decoration: none; + + &:hover { + cursor: pointer; + color: $blue; + border: none; + background-color: transparent; + } +} diff --git a/src/css/_form.scss b/src/css/_form.scss index a55c3fb..9bb247c 100644 --- a/src/css/_form.scss +++ b/src/css/_form.scss @@ -63,8 +63,13 @@ input { text-align: center; background-color: #eeeeee; border: 1px solid #cccccc; - border-left: none; line-height: 18px; + &--right { + border-left: none; + } + &--left { + border-right: none; + } } } } diff --git a/src/css/options.scss b/src/css/options.scss index d5eccfa..28091ba 100644 --- a/src/css/options.scss +++ b/src/css/options.scss @@ -20,6 +20,11 @@ margin: 1rem 0 2rem; } + h3 { + font-size: 1.1rem; + margin: 1rem 0 1rem; + } + label { font-weight: normal; margin-bottom: 5px; @@ -39,15 +44,10 @@ color: $red; } - .moco-bx-override-hosts { - &-container { - padding: 0 20px; - text-align: center; - } - &-btn { - cursor: pointer; - text-decoration: underline; - } + &__host-overrides { + padding-bottom: 1rem; + text-align: center; + font-weight: normal; } } } diff --git a/src/js/components/Options.js b/src/js/components/Options.js index 91804da..0f8e41b 100644 --- a/src/js/components/Options.js +++ b/src/js/components/Options.js @@ -4,7 +4,24 @@ import { observer } from "mobx-react" import { isChrome, getSettings, setStorage } from "utils/browser" import ApiClient from "api/Client" import remoteServices from "../remoteServices" -import { pipe, prop, map, sortedUniqBy, filter } from "lodash/fp" +import { pipe, toPairs, fromPairs, prop, map, sortedUniqBy, filter } from "lodash/fp" + +function upperCaseFirstLetter(input) { + return input[0].toUpperCase() + input.slice(1) +} + +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 { @@ -13,33 +30,22 @@ class Options extends Component { @observable hostOverrides = {} @observable errorMessage = null @observable isSuccess = false - @observable servicesHostOverrideList = [] @observable showHostOverrideOptions = false componentDidMount() { - this.servicesHostOverrideList = pipe( - filter(prop("allowHostOverride")), - map((remoteService) => ({ - name: remoteService.name, - host: remoteService.host, - })), - sortedUniqBy("name"), - )(remoteServices) - getSettings(false).then((storeData) => { this.subdomain = storeData.subdomain || "" this.apiKey = storeData.apiKey || "" - this.hostOverrides = storeData.hostOverrides || {} + this.hostOverrides = storeData.hostOverrides }) } - onChange = (event) => { + handleChange = (event) => { this[event.target.name] = event.target.value.trim() } - onChangeHostOverrides = (event) => { - // ensure to remove path (and trailing slash) from URL, as this can cause problems otherwise - this.hostOverrides[event.target.name] = this.removePathFromUrl(event.target.value.trim()) + handleChangeHostOverrides = (event) => { + this.hostOverrides[event.target.name] = event.target.value.trim() } toggleHostOverrideOptions = () => { @@ -54,7 +60,11 @@ class Options extends Component { subdomain: this.subdomain, apiKey: this.apiKey, settingTimeTrackingHHMM: false, - hostOverrides: this.hostOverrides, + hostOverrides: pipe( + toPairs, + map(([key, url]) => [key, removePathFromUrl(url)]), + fromPairs, + )(this.hostOverrides), }).then(() => { const { version } = chrome.runtime.getManifest() const apiClient = new ApiClient({ @@ -77,10 +87,6 @@ class Options extends Component { }) } - removePathFromUrl = (url) => { - return url.replace(/(\.[a-z]+)\/.*$/, "$1") - } - handleInputKeyDown = (event) => { if (event.key === "Enter") { this.handleSubmit() @@ -105,9 +111,9 @@ class Options extends Component { name="subdomain" value={this.subdomain} onKeyDown={this.handleInputKeyDown} - onChange={this.onChange} + onChange={this.handleChange} /> - .mocoapp.com + .mocoapp.com
@@ -117,33 +123,44 @@ class Options extends Component { name="apiKey" value={this.apiKey} onKeyDown={this.handleInputKeyDown} - onChange={this.onChange} + onChange={this.handleChange} />

Den API-Schlüssel findest du in deinem Profil unter "Integrationen".

-
{!this.showHostOverrideOptions && ( -
- Zeige Optionen zum Überschreiben
der Service Hosts
+
+ + URLs für Dienste anpassen? + +
+ )} + {this.showHostOverrideOptions && ( +
+

URLs für Dienste

+ {overridableRemoveServices.map((remoteService) => ( +
+
+ + {upperCaseFirstLetter(remoteService.name)} + + +
+
+ ))}
)} - {this.showHostOverrideOptions && - this.servicesHostOverrideList.map((remoteService) => ( -
- - -
- ))} -
diff --git a/src/js/remoteServices.js b/src/js/remoteServices.js index ebd9362..5d96906 100644 --- a/src/js/remoteServices.js +++ b/src/js/remoteServices.js @@ -1,10 +1,7 @@ const projectRegex = /\[([\w-]+)\]/ -const projectIdentifierBySelector = selector => document => - document - .querySelector(selector) - ?.textContent?.trim() - ?.match(projectRegex)?.[1] +const projectIdentifierBySelector = (selector) => (document) => + document.querySelector(selector)?.textContent?.trim()?.match(projectRegex)?.[1] export default { asana: { @@ -14,7 +11,7 @@ export default { [/^__HOST__\/0\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], [/^__HOST__\/0\/search\/([^/]+)\/(\d+)/, ["domainUserId", "id"]], ], - description: document => + description: (document) => document.querySelector(".ItemRow--highlighted textarea")?.textContent?.trim() || document.querySelector(".ItemRow--focused textarea")?.textContent?.trim() || document.querySelector(".SingleTaskPane textarea")?.textContent?.trim() || @@ -28,7 +25,7 @@ export default { 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(), + description: (document) => document.querySelector(".js-issue-title")?.textContent?.trim(), projectId: projectIdentifierBySelector(".js-issue-title"), allowHostOverride: true, }, @@ -73,12 +70,12 @@ export default { name: "meistertask", host: "https://www.meistertask.com", urlPatterns: ["/app/task/:id/:slug"], - description: document => { + description: (document) => { const json = document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}" const data = JSON.parse(json) return data.taskName }, - projectId: document => { + projectId: (document) => { const json = document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}" const data = JSON.parse(json) const match = data.taskName?.match(projectRegex) || data.projectName?.match(projectRegex) @@ -93,7 +90,7 @@ export default { urlPatterns: ["__HOST__/c/:id/:title"], description: (document, service, { title }) => document.querySelector(".js-title-helper")?.textContent?.trim() || title, - projectId: document => + projectId: (document) => projectIdentifierBySelector(".js-title-helper")(document) || projectIdentifierBySelector(".js-board-editing-target")(document), allowHostOverride: false, @@ -103,7 +100,7 @@ export default { name: "youtrack", host: "https://:org.myjetbrains.com", urlPatterns: ["__HOST__/youtrack/issue/:id"], - description: document => document.querySelector("yt-issue-body h1")?.textContent?.trim(), + description: (document) => document.querySelector("yt-issue-body h1")?.textContent?.trim(), projectId: projectIdentifierBySelector("yt-issue-body h1"), allowHostOverride: true, }, @@ -120,7 +117,7 @@ export default { queryParams: { id: ["t", "ot"], }, - description: document => document.querySelector(".title-field-ghost")?.textContent?.trim(), + description: (document) => document.querySelector(".title-field-ghost")?.textContent?.trim(), projectId: projectIdentifierBySelector(".header-title__main"), allowHostOverride: false, }, @@ -129,7 +126,7 @@ export default { name: "wunderlist", host: "https://www.wunderlist.com", urlPatterns: ["__HOST__/(webapp)#/tasks/:id(/*)"], - description: document => + description: (document) => document .querySelector(".taskItem.selected .taskItem-titleWrapper-title") ?.textContent?.trim(), diff --git a/src/js/utils/browser.js b/src/js/utils/browser.js index 92d81cf..721029d 100644 --- a/src/js/utils/browser.js +++ b/src/js/utils/browser.js @@ -4,32 +4,40 @@ 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 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 => { + return new Promise((resolve) => { + chrome.storage.sync.get(keys, (data) => { if (withDefaultSubdomain) { data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN } + data.hostOverrides = { ...DEFAULT_HOST_OVERRIDES, ...(data.hostOverrides || {}) } resolve({ ...data, version }) }) }) } else { - return browser.storage.sync.get(keys).then(data => { + return browser.storage.sync.get(keys).then((data) => { if (withDefaultSubdomain) { data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN } + data.hostOverrides = { ...DEFAULT_HOST_OVERRIDES, ...(data.hostOverrides || {}) } return { ...data, 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 { @@ -37,9 +45,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) } @@ -49,4 +57,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) diff --git a/webpack.base.config.js b/webpack.base.config.js index e99e890..1396b22 100644 --- a/webpack.base.config.js +++ b/webpack.base.config.js @@ -62,7 +62,7 @@ module.exports = (env) => { }, plugins: [ new CleanWebpackPlugin({ - cleanAfterEveryBuildPatterns: ["!manifest.json"], + cleanAfterEveryBuildPatterns: ["!manifest.json", "!*.html"], }), new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),