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
+
+ )}
+ {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),