Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1533c2261f | ||
|
|
d8398fca5f | ||
|
|
02a0bec738 | ||
|
|
0f5172a820 | ||
|
|
a3f94738b6 | ||
|
|
c153eb6c91 | ||
|
|
87aaa99276 | ||
|
|
e6b6f67814 | ||
|
|
1f8bc33830 | ||
|
|
8e55c13d72 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -28,3 +28,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
- Update README with example configuration and instructions for local installation
|
||||
|
||||
## [1.0.22] - 2019-03-28
|
||||
### Changed
|
||||
- Change the default value of subdomain to `unset` to have a well-formed URL.
|
||||
|
||||
## [1.1.0] - 2019-03-30
|
||||
### Added
|
||||
- Read project identifier from Asana project title
|
||||
- Add support for meistertask.com
|
||||
|
||||
### Fixed
|
||||
- Link logo in modal to MOCO activities page
|
||||
- Set full url on service, including query params
|
||||
|
||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019, hundertzehn GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "moco-browser-extensions",
|
||||
"description": "Browser plugin for MOCO",
|
||||
"version": "1.0.21",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "yarn start:chrome",
|
||||
"start:chrome": "node_modules/.bin/webpack --config webpack.chrome.config.js --watch --env.browser chrome --env.NODE_ENV development",
|
||||
@@ -15,6 +16,7 @@
|
||||
"test:watch": "node_modules/.bin/jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.4.0",
|
||||
"@bugsnag/js": "^5.2.0",
|
||||
"@bugsnag/plugin-react": "^5.2.0",
|
||||
"axios": "^0.18.0",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#moco-bx-root {
|
||||
font-family: $font-family;
|
||||
color: $font-color;
|
||||
pointer-events: all;
|
||||
|
||||
.moco-bx-bubble {
|
||||
box-sizing: content-box;
|
||||
@@ -48,6 +49,7 @@
|
||||
#moco-bx-popup-root {
|
||||
font-family: $font-family;
|
||||
color: $font-color;
|
||||
pointer-events: all;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
@@ -58,16 +60,15 @@
|
||||
}
|
||||
|
||||
.moco-bx-popup {
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 2000; /* Sit on top */
|
||||
padding-top: 100px; /* Location of the box */
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
padding-top: 100px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0, 0, 0); /* Fallback color */
|
||||
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.moco-bx-popup-content {
|
||||
background-color: white;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@babel/polyfill"
|
||||
import ApiClient from "api/Client"
|
||||
import {
|
||||
isChrome,
|
||||
|
||||
@@ -36,6 +36,7 @@ class App extends Component {
|
||||
projectId: PropTypes.string,
|
||||
taskId: PropTypes.string
|
||||
}),
|
||||
subdomain: PropTypes.string,
|
||||
activities: PropTypes.array,
|
||||
schedules: PropTypes.array,
|
||||
projects: PropTypes.array,
|
||||
@@ -62,12 +63,14 @@ class App extends Component {
|
||||
const { service, projects, lastProjectId, lastTaskId } = this.props
|
||||
|
||||
const project =
|
||||
findProjectByValue(this.changeset.assignment_id)(projects) ||
|
||||
findProjectByIdentifier(service?.projectId)(projects) ||
|
||||
findProjectByValue(Number(lastProjectId))(projects) ||
|
||||
head(projects)
|
||||
|
||||
const task =
|
||||
findTask(service?.taskId || lastTaskId)(project) || head(project?.tasks)
|
||||
findTask(this.changeset.task_id || service?.taskId || lastTaskId)(project) ||
|
||||
head(project?.tasks)
|
||||
|
||||
const defaults = {
|
||||
remote_service: service?.name,
|
||||
@@ -108,7 +111,7 @@ class App extends Component {
|
||||
|
||||
if (name === "assignment_id") {
|
||||
const project = findProjectByValue(value)(projects)
|
||||
this.changeset.task_id = head(project?.tasks).value || null
|
||||
this.changeset.task_id = head(project?.tasks)?.value
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,6 +148,7 @@ class App extends Component {
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
subdomain,
|
||||
projects,
|
||||
activities,
|
||||
schedules,
|
||||
@@ -179,7 +183,7 @@ class App extends Component {
|
||||
>
|
||||
{props => (
|
||||
<animated.div className="moco-bx-app-container" style={props}>
|
||||
<Header />
|
||||
<Header subdomain={subdomain} />
|
||||
<Observer>
|
||||
{() => (
|
||||
<>
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import logoUrl from "images/logo.png"
|
||||
|
||||
const Bubble = ({ bookedHours, onClick }) => (
|
||||
<div className="moco-bx-bubble-inner" onClick={onClick}>
|
||||
const Bubble = ({ bookedHours }) => (
|
||||
<div className="moco-bx-bubble-inner">
|
||||
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
|
||||
{bookedHours > 0 ? (
|
||||
<span className="moco-bx-booked-hours">{bookedHours.toFixed(2)}</span>
|
||||
@@ -12,8 +12,7 @@ const Bubble = ({ bookedHours, onClick }) => (
|
||||
)
|
||||
|
||||
Bubble.propTypes = {
|
||||
bookedHours: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired
|
||||
bookedHours: PropTypes.number
|
||||
}
|
||||
|
||||
Bubble.defaultProps = {
|
||||
|
||||
@@ -52,6 +52,7 @@ class Popup extends Component {
|
||||
const serializedProps = serializeProps([
|
||||
"loading",
|
||||
"service",
|
||||
"subdomain",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"roundTimeEntries",
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import logoUrl from "images/logo.png"
|
||||
|
||||
const Header = () => (
|
||||
const Header = ({ subdomain }) => (
|
||||
<div className="moco-bx-logo__container">
|
||||
<a href={`https://${subdomain}.mocoapp.com/activities`} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
className="moco-bx-logo"
|
||||
src={chrome.extension.getURL(logoUrl)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
subdomain: PropTypes.string
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
||||
@@ -43,15 +43,12 @@ chrome.runtime.onConnect.addListener(function(port) {
|
||||
<animated.div
|
||||
className="moco-bx-bubble"
|
||||
style={{ ...props, ...service.position }}
|
||||
>
|
||||
<Bubble
|
||||
key={service.url}
|
||||
bookedHours={bookedHours}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
messenger.postMessage({ type: "togglePopup" })
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Bubble key={service.url} bookedHours={bookedHours} />
|
||||
</animated.div>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../css/popup.scss"
|
||||
const parsedProps = parseProps([
|
||||
"loading",
|
||||
"service",
|
||||
"subdomain",
|
||||
"projects",
|
||||
"activities",
|
||||
"schedules",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const projectRegex = /\[([\w-]+)\]/
|
||||
|
||||
export default {
|
||||
asana: {
|
||||
name: "asana",
|
||||
@@ -15,7 +17,14 @@ export default {
|
||||
document
|
||||
.querySelector(".ItemRow--focused textarea")
|
||||
?.textContent?.trim() ||
|
||||
document.querySelector(".SingleTaskPane textarea")?.textContent?.trim()
|
||||
document.querySelector(".SingleTaskPane textarea")?.textContent?.trim(),
|
||||
projectId: document => {
|
||||
const match = document
|
||||
.querySelector(".ProjectPageHeader-projectName")
|
||||
?.textContent?.trim()
|
||||
?.match(projectRegex)
|
||||
return match && match[1]
|
||||
}
|
||||
},
|
||||
|
||||
"github-pr": {
|
||||
@@ -29,7 +38,7 @@ export default {
|
||||
const match = document
|
||||
.querySelector(".js-issue-title")
|
||||
?.textContent.trim()
|
||||
?.match(/^\[(\d+)\]/)
|
||||
?.match(projectRegex)
|
||||
return match && match[1]
|
||||
}
|
||||
},
|
||||
@@ -68,6 +77,24 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
meistertask: {
|
||||
name: "meistertask",
|
||||
urlPatterns: ["https\\://www.meistertask.com/app/task/:id/:slug"],
|
||||
description: document => {
|
||||
const json =
|
||||
document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}"
|
||||
const data = JSON.parse(json)
|
||||
return data.taskName
|
||||
},
|
||||
projectId: document => {
|
||||
const json =
|
||||
document.getElementById("mt-toggl-data")?.dataset?.togglJson || "{}"
|
||||
const data = JSON.parse(json)
|
||||
const match = data.projectName?.match(projectRegex)
|
||||
return match && match[1]
|
||||
}
|
||||
},
|
||||
|
||||
trello: {
|
||||
name: "trello",
|
||||
urlPatterns: ["https\\://trello.com/c/:id/:title"],
|
||||
|
||||
@@ -2,6 +2,8 @@ import { head } from "lodash/fp"
|
||||
export const isChrome = () => typeof browser === "undefined" && chrome
|
||||
export const isFirefox = () => typeof browser !== "undefined" && chrome
|
||||
|
||||
const DEFAULT_SUBDOMAIN = "unset"
|
||||
|
||||
export const getSettings = (withDefaultSubdomain = true) => {
|
||||
const keys = ["subdomain", "apiKey"]
|
||||
const { version } = chrome.runtime.getManifest()
|
||||
@@ -9,7 +11,7 @@ export const getSettings = (withDefaultSubdomain = true) => {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.sync.get(keys, data => {
|
||||
if (withDefaultSubdomain) {
|
||||
data.subdomain = data.subdomain || "__unset__"
|
||||
data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
|
||||
}
|
||||
resolve({ ...data, version })
|
||||
})
|
||||
@@ -17,7 +19,7 @@ export const getSettings = (withDefaultSubdomain = true) => {
|
||||
} else {
|
||||
return browser.storage.sync.get(keys).then(data => {
|
||||
if (withDefaultSubdomain) {
|
||||
data.subdomain = data.subdomain || "__unset__"
|
||||
data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
|
||||
}
|
||||
return { ...data, version }
|
||||
})
|
||||
|
||||
@@ -76,26 +76,25 @@ export function togglePopup(tab, { messenger }) {
|
||||
}
|
||||
}
|
||||
|
||||
function openPopup(tab, { service, messenger }) {
|
||||
async function openPopup(tab, { service, messenger }) {
|
||||
messenger.postMessage(tab, { type: "openPopup", payload: { loading: true } })
|
||||
|
||||
const fromDate = getStartOfWeek()
|
||||
const toDate = getEndOfWeek()
|
||||
getSettings()
|
||||
.then(settings => new ApiClient(settings))
|
||||
.then(apiClient =>
|
||||
Promise.all([
|
||||
const settings = await getSettings()
|
||||
const apiClient = new ApiClient(settings)
|
||||
try {
|
||||
const responses = await Promise.all([
|
||||
apiClient.login(service),
|
||||
apiClient.projects(),
|
||||
apiClient.activities(fromDate, toDate),
|
||||
apiClient.schedules(fromDate, toDate)
|
||||
])
|
||||
)
|
||||
.then(responses => {
|
||||
const action = {
|
||||
type: "openPopup",
|
||||
payload: {
|
||||
service,
|
||||
subdomain: settings.subdomain,
|
||||
lastProjectId: get("[0].data.last_project_id", responses),
|
||||
lastTaskId: get("[0].data.last_task_id", responses),
|
||||
roundTimeEntries: get("[0].data.round_time_entries", responses),
|
||||
@@ -108,8 +107,7 @@ function openPopup(tab, { service, messenger }) {
|
||||
}
|
||||
}
|
||||
messenger.postMessage(tab, action)
|
||||
})
|
||||
.catch(error => {
|
||||
} catch (error) {
|
||||
let errorType, errorMessage
|
||||
if (error.response?.status === 401) {
|
||||
errorType = ERROR_UNAUTHORIZED
|
||||
@@ -123,5 +121,5 @@ function openPopup(tab, { service, messenger }) {
|
||||
type: "openPopup",
|
||||
payload: { errorType, errorMessage }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export const createMatcher = remoteServices => {
|
||||
return {
|
||||
...match,
|
||||
...service,
|
||||
url,
|
||||
url: tabUrl,
|
||||
match
|
||||
}
|
||||
}
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -607,6 +607,14 @@
|
||||
"@babel/helper-regex" "^7.0.0"
|
||||
regexpu-core "^4.1.3"
|
||||
|
||||
"@babel/polyfill@^7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.4.0.tgz#90f9d68ae34ac42ab4b4aa03151848f536960218"
|
||||
integrity sha512-bVsjsrtsDflIHp5I6caaAa2V25Kzn50HKPL6g3X0P0ni1ks+58cPB8Mz6AOKVuRPgaVdq/OwEUc/1vKqX+Mo4A==
|
||||
dependencies:
|
||||
core-js "^2.6.5"
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/preset-env@^7.2.2":
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db"
|
||||
@@ -2021,6 +2029,11 @@ copyfiles@^2.1.0:
|
||||
through2 "^2.0.1"
|
||||
yargs "^11.0.0"
|
||||
|
||||
core-js@^2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
|
||||
integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
@@ -6160,6 +6173,11 @@ regenerator-runtime@^0.12.0:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||
|
||||
regenerator-runtime@^0.13.2:
|
||||
version "0.13.2"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
|
||||
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
|
||||
|
||||
regenerator-transform@^0.13.3:
|
||||
version "0.13.3"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"
|
||||
|
||||
Reference in New Issue
Block a user