Compare commits

...

10 Commits

Author SHA1 Message Date
Manuel Bouza
1533c2261f Update projecte regex to match on alphanumeric values with hyphens 2019-03-30 07:51:59 +01:00
Manuel Bouza
d8398fca5f Pump version to 1.1.0 2019-03-30 07:26:25 +01:00
Manuel Bouza
02a0bec738 Browser extension fixes (#8)
* Set full url on service

* Link logo to `/activities` in modal

* Update changelog

* Honor the selected task and set the correct billability
2019-03-30 06:59:18 +01:00
Manuel Bouza
0f5172a820 Read project identifier in asana service (#7) 2019-03-30 06:54:28 +01:00
Tobias Miesel
a3f94738b6 Merge pull request #6 from hundertzehn/feature/meistertask
Add support for Meistertask
2019-03-30 06:52:54 +01:00
Manuel Bouza
c153eb6c91 Update regex for project identifier to match anywhere 2019-03-29 22:26:35 +01:00
Manuel Bouza
87aaa99276 Parse description and projectId 2019-03-29 22:24:32 +01:00
Manuel Bouza
e6b6f67814 Add support for Meistertask service 2019-03-29 22:24:32 +01:00
Manuel Bouza
1f8bc33830 Change default subdomain to unset 2019-03-28 09:12:34 +01:00
Manuel Bouza
8e55c13d72 Add license information 2019-03-26 16:57:23 +01:00
16 changed files with 167 additions and 76 deletions

View File

@@ -28,3 +28,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Update README with example configuration and instructions for local installation - 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
View 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.

View File

@@ -1,7 +1,8 @@
{ {
"name": "moco-browser-extensions", "name": "moco-browser-extensions",
"description": "Browser plugin for MOCO", "description": "Browser plugin for MOCO",
"version": "1.0.21", "version": "1.1.0",
"license": "MIT",
"scripts": { "scripts": {
"start": "yarn start:chrome", "start": "yarn start:chrome",
"start:chrome": "node_modules/.bin/webpack --config webpack.chrome.config.js --watch --env.browser chrome --env.NODE_ENV development", "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" "test:watch": "node_modules/.bin/jest --watch"
}, },
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.4.0",
"@bugsnag/js": "^5.2.0", "@bugsnag/js": "^5.2.0",
"@bugsnag/plugin-react": "^5.2.0", "@bugsnag/plugin-react": "^5.2.0",
"axios": "^0.18.0", "axios": "^0.18.0",

View File

@@ -4,6 +4,7 @@
#moco-bx-root { #moco-bx-root {
font-family: $font-family; font-family: $font-family;
color: $font-color; color: $font-color;
pointer-events: all;
.moco-bx-bubble { .moco-bx-bubble {
box-sizing: content-box; box-sizing: content-box;
@@ -48,6 +49,7 @@
#moco-bx-popup-root { #moco-bx-popup-root {
font-family: $font-family; font-family: $font-family;
color: $font-color; color: $font-color;
pointer-events: all;
iframe { iframe {
border: 0; border: 0;
@@ -58,16 +60,15 @@
} }
.moco-bx-popup { .moco-bx-popup {
position: fixed; /* Stay in place */ position: fixed;
z-index: 2000; /* Sit on top */ z-index: 2000;
padding-top: 100px; /* Location of the box */ padding-top: 100px;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; /* Full width */ width: 100%;
height: 100%; /* Full height */ height: 100%;
overflow: auto; /* Enable scroll if needed */ overflow: auto;
background-color: rgb(0, 0, 0); /* Fallback color */ background-color: rgba(0, 0, 0, 0.4);
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
.moco-bx-popup-content { .moco-bx-popup-content {
background-color: white; background-color: white;

View File

@@ -1,3 +1,4 @@
import "@babel/polyfill"
import ApiClient from "api/Client" import ApiClient from "api/Client"
import { import {
isChrome, isChrome,

View File

@@ -36,6 +36,7 @@ class App extends Component {
projectId: PropTypes.string, projectId: PropTypes.string,
taskId: PropTypes.string taskId: PropTypes.string
}), }),
subdomain: PropTypes.string,
activities: PropTypes.array, activities: PropTypes.array,
schedules: PropTypes.array, schedules: PropTypes.array,
projects: PropTypes.array, projects: PropTypes.array,
@@ -62,12 +63,14 @@ class App extends Component {
const { service, projects, lastProjectId, lastTaskId } = this.props const { service, projects, lastProjectId, lastTaskId } = this.props
const project = const project =
findProjectByValue(this.changeset.assignment_id)(projects) ||
findProjectByIdentifier(service?.projectId)(projects) || findProjectByIdentifier(service?.projectId)(projects) ||
findProjectByValue(Number(lastProjectId))(projects) || findProjectByValue(Number(lastProjectId))(projects) ||
head(projects) head(projects)
const task = const task =
findTask(service?.taskId || lastTaskId)(project) || head(project?.tasks) findTask(this.changeset.task_id || service?.taskId || lastTaskId)(project) ||
head(project?.tasks)
const defaults = { const defaults = {
remote_service: service?.name, remote_service: service?.name,
@@ -108,7 +111,7 @@ class App extends Component {
if (name === "assignment_id") { if (name === "assignment_id") {
const project = findProjectByValue(value)(projects) 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() { render() {
const { const {
loading, loading,
subdomain,
projects, projects,
activities, activities,
schedules, schedules,
@@ -179,7 +183,7 @@ class App extends Component {
> >
{props => ( {props => (
<animated.div className="moco-bx-app-container" style={props}> <animated.div className="moco-bx-app-container" style={props}>
<Header /> <Header subdomain={subdomain} />
<Observer> <Observer>
{() => ( {() => (
<> <>

View File

@@ -2,8 +2,8 @@ import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import logoUrl from "images/logo.png" import logoUrl from "images/logo.png"
const Bubble = ({ bookedHours, onClick }) => ( const Bubble = ({ bookedHours }) => (
<div className="moco-bx-bubble-inner" onClick={onClick}> <div className="moco-bx-bubble-inner">
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} /> <img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
{bookedHours > 0 ? ( {bookedHours > 0 ? (
<span className="moco-bx-booked-hours">{bookedHours.toFixed(2)}</span> <span className="moco-bx-booked-hours">{bookedHours.toFixed(2)}</span>
@@ -12,8 +12,7 @@ const Bubble = ({ bookedHours, onClick }) => (
) )
Bubble.propTypes = { Bubble.propTypes = {
bookedHours: PropTypes.number, bookedHours: PropTypes.number
onClick: PropTypes.func.isRequired
} }
Bubble.defaultProps = { Bubble.defaultProps = {

View File

@@ -52,6 +52,7 @@ class Popup extends Component {
const serializedProps = serializeProps([ const serializedProps = serializeProps([
"loading", "loading",
"service", "service",
"subdomain",
"lastProjectId", "lastProjectId",
"lastTaskId", "lastTaskId",
"roundTimeEntries", "roundTimeEntries",

View File

@@ -1,13 +1,20 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import logoUrl from "images/logo.png" import logoUrl from "images/logo.png"
const Header = () => ( const Header = ({ subdomain }) => (
<div className="moco-bx-logo__container"> <div className="moco-bx-logo__container">
<a href={`https://${subdomain}.mocoapp.com/activities`} target="_blank" rel="noopener noreferrer">
<img <img
className="moco-bx-logo" className="moco-bx-logo"
src={chrome.extension.getURL(logoUrl)} src={chrome.extension.getURL(logoUrl)}
/> />
</a>
</div> </div>
) )
Header.propTypes = {
subdomain: PropTypes.string
}
export default Header export default Header

View File

@@ -43,15 +43,12 @@ chrome.runtime.onConnect.addListener(function(port) {
<animated.div <animated.div
className="moco-bx-bubble" className="moco-bx-bubble"
style={{ ...props, ...service.position }} style={{ ...props, ...service.position }}
>
<Bubble
key={service.url}
bookedHours={bookedHours}
onClick={event => { onClick={event => {
event.stopPropagation() event.stopPropagation()
messenger.postMessage({ type: "togglePopup" }) messenger.postMessage({ type: "togglePopup" })
}} }}
/> >
<Bubble key={service.url} bookedHours={bookedHours} />
</animated.div> </animated.div>
)) ))
} }

View File

@@ -9,6 +9,7 @@ import "../css/popup.scss"
const parsedProps = parseProps([ const parsedProps = parseProps([
"loading", "loading",
"service", "service",
"subdomain",
"projects", "projects",
"activities", "activities",
"schedules", "schedules",

View File

@@ -1,3 +1,5 @@
const projectRegex = /\[([\w-]+)\]/
export default { export default {
asana: { asana: {
name: "asana", name: "asana",
@@ -15,7 +17,14 @@ export default {
document document
.querySelector(".ItemRow--focused textarea") .querySelector(".ItemRow--focused textarea")
?.textContent?.trim() || ?.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": { "github-pr": {
@@ -29,7 +38,7 @@ export default {
const match = document const match = document
.querySelector(".js-issue-title") .querySelector(".js-issue-title")
?.textContent.trim() ?.textContent.trim()
?.match(/^\[(\d+)\]/) ?.match(projectRegex)
return match && match[1] 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: { trello: {
name: "trello", name: "trello",
urlPatterns: ["https\\://trello.com/c/:id/:title"], urlPatterns: ["https\\://trello.com/c/:id/:title"],

View File

@@ -2,6 +2,8 @@ import { head } from "lodash/fp"
export const isChrome = () => typeof browser === "undefined" && chrome export const isChrome = () => typeof browser === "undefined" && chrome
export const isFirefox = () => typeof browser !== "undefined" && chrome export const isFirefox = () => typeof browser !== "undefined" && chrome
const DEFAULT_SUBDOMAIN = "unset"
export const getSettings = (withDefaultSubdomain = true) => { export const getSettings = (withDefaultSubdomain = true) => {
const keys = ["subdomain", "apiKey"] const keys = ["subdomain", "apiKey"]
const { version } = chrome.runtime.getManifest() const { version } = chrome.runtime.getManifest()
@@ -9,7 +11,7 @@ export const getSettings = (withDefaultSubdomain = true) => {
return new Promise(resolve => { return new Promise(resolve => {
chrome.storage.sync.get(keys, data => { chrome.storage.sync.get(keys, data => {
if (withDefaultSubdomain) { if (withDefaultSubdomain) {
data.subdomain = data.subdomain || "__unset__" data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
} }
resolve({ ...data, version }) resolve({ ...data, version })
}) })
@@ -17,7 +19,7 @@ export const getSettings = (withDefaultSubdomain = true) => {
} else { } else {
return browser.storage.sync.get(keys).then(data => { return browser.storage.sync.get(keys).then(data => {
if (withDefaultSubdomain) { if (withDefaultSubdomain) {
data.subdomain = data.subdomain || "__unset__" data.subdomain = data.subdomain || DEFAULT_SUBDOMAIN
} }
return { ...data, version } return { ...data, version }
}) })

View File

@@ -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 } }) messenger.postMessage(tab, { type: "openPopup", payload: { loading: true } })
const fromDate = getStartOfWeek() const fromDate = getStartOfWeek()
const toDate = getEndOfWeek() const toDate = getEndOfWeek()
getSettings() const settings = await getSettings()
.then(settings => new ApiClient(settings)) const apiClient = new ApiClient(settings)
.then(apiClient => try {
Promise.all([ const responses = await Promise.all([
apiClient.login(service), apiClient.login(service),
apiClient.projects(), apiClient.projects(),
apiClient.activities(fromDate, toDate), apiClient.activities(fromDate, toDate),
apiClient.schedules(fromDate, toDate) apiClient.schedules(fromDate, toDate)
]) ])
)
.then(responses => {
const action = { const action = {
type: "openPopup", type: "openPopup",
payload: { payload: {
service, service,
subdomain: settings.subdomain,
lastProjectId: get("[0].data.last_project_id", responses), lastProjectId: get("[0].data.last_project_id", responses),
lastTaskId: get("[0].data.last_task_id", responses), lastTaskId: get("[0].data.last_task_id", responses),
roundTimeEntries: get("[0].data.round_time_entries", responses), roundTimeEntries: get("[0].data.round_time_entries", responses),
@@ -108,8 +107,7 @@ function openPopup(tab, { service, messenger }) {
} }
} }
messenger.postMessage(tab, action) messenger.postMessage(tab, action)
}) } catch (error) {
.catch(error => {
let errorType, errorMessage let errorType, errorMessage
if (error.response?.status === 401) { if (error.response?.status === 401) {
errorType = ERROR_UNAUTHORIZED errorType = ERROR_UNAUTHORIZED
@@ -123,5 +121,5 @@ function openPopup(tab, { service, messenger }) {
type: "openPopup", type: "openPopup",
payload: { errorType, errorMessage } payload: { errorType, errorMessage }
}) })
}) }
} }

View File

@@ -88,7 +88,7 @@ export const createMatcher = remoteServices => {
return { return {
...match, ...match,
...service, ...service,
url, url: tabUrl,
match match
} }
} }

View File

@@ -607,6 +607,14 @@
"@babel/helper-regex" "^7.0.0" "@babel/helper-regex" "^7.0.0"
regexpu-core "^4.1.3" 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": "@babel/preset-env@^7.2.2":
version "7.3.1" version "7.3.1"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" 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" through2 "^2.0.1"
yargs "^11.0.0" 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: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== 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: regenerator-transform@^0.13.3:
version "0.13.3" version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb"