feature/wrike (#17)

* Fix code styles

* Add support for WRIKE

* Add tests
This commit is contained in:
Manuel Bouza 2019-04-26 13:05:14 +02:00 committed by Tobias Miesel
parent 25773cc661
commit a9d1726707
9 changed files with 118 additions and 83 deletions

View File

@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for starting/stopping a timer - Add support for starting/stopping a timer
- Show hours as HH:MM or decimal in the Bubble, depending on setting in MOCO - Show hours as HH:MM or decimal in the Bubble, depending on setting in MOCO
## [1.2.0] - 2019-04-26
### Added
- Add support for wrike.com
## [1.1.5] - 2019-04-24 ## [1.1.5] - 2019-04-24
### Fixed ### Fixed

View File

@ -1,7 +1,7 @@
{ {
"name": "moco-browser-extensions", "name": "moco-browser-extensions",
"description": "Browser plugin for MOCO", "description": "Browser plugin for MOCO",
"version": "1.1.5", "version": "1.2.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "yarn start:chrome", "start": "yarn start:chrome",

View File

@ -14,8 +14,7 @@
width: 60px; width: 60px;
background-color: white; background-color: white;
border-radius: 50%; border-radius: 50%;
box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05), box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05), 2px 2px 15px 4px rgba(0, 0, 0, 0.05);
2px 2px 15px 4px rgba(0, 0, 0, 0.05);
padding: 5px; padding: 5px;
z-index: 9999; z-index: 9999;
@ -69,6 +68,7 @@
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
z-index: 9999;
.moco-bx-popup-content { .moco-bx-popup-content {
background-color: white; background-color: white;

View File

@ -1,17 +1,8 @@
import "@babel/polyfill" import "@babel/polyfill"
import ApiClient from "api/Client" import ApiClient from "api/Client"
import { import { isChrome, getCurrentTab, getSettings, isBrowserTab } from "utils/browser"
isChrome,
getCurrentTab,
getSettings,
isBrowserTab
} from "utils/browser"
import { BackgroundMessenger } from "utils/messaging" import { BackgroundMessenger } from "utils/messaging"
import { import { tabUpdated, settingsChanged, togglePopup } from "utils/messageHandlers"
tabUpdated,
settingsChanged,
togglePopup
} from "utils/messageHandlers"
const messenger = new BackgroundMessenger() const messenger = new BackgroundMessenger()
@ -47,8 +38,8 @@ chrome.runtime.onMessage.addListener(action => {
type: "showBubble", type: "showBubble",
payload: { payload: {
bookedHours: parseFloat(data[0]?.hours) || 0, bookedHours: parseFloat(data[0]?.hours) || 0,
service service,
} },
}) })
}) })
}) })
@ -56,7 +47,7 @@ chrome.runtime.onMessage.addListener(action => {
if (error.response?.status === 422) { if (error.response?.status === 422) {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: "setFormErrors", type: "setFormErrors",
payload: error.response.data payload: error.response.data,
}) })
} }
}) })

View File

@ -95,6 +95,25 @@ export default {
description: document => document.querySelector("yt-issue-body h1")?.textContent?.trim(), description: document => document.querySelector("yt-issue-body h1")?.textContent?.trim(),
}, },
wrike: {
name: "wrike",
urlPatterns: [
"https\\://www.wrike.com/workspace.htm#path=mywork",
"https\\://www.wrike.com/workspace.htm#path=folder",
],
queryParams: {
id: ["t", "ot"],
},
description: document => document.querySelector(".title-field-ghost")?.textContent?.trim(),
projectId: document => {
const match = document
.querySelector(".header-title__main")
?.textContent?.trim()
?.match(projectRegex)
return match && match[1]
},
},
wunderlist: { wunderlist: {
name: "wunderlist", name: "wunderlist",
urlPatterns: ["https\\://www.wunderlist.com/(webapp)#/tasks/:id(/*)"], urlPatterns: ["https\\://www.wunderlist.com/(webapp)#/tasks/:id(/*)"],

View File

@ -9,7 +9,7 @@ import {
get, get,
find, find,
curry, curry,
pick pick,
} from "lodash/fp" } from "lodash/fp"
import { format } from "date-fns" import { format } from "date-fns"
@ -20,6 +20,7 @@ export const ERROR_UPGRADE_REQUIRED = "upgrade-required"
export const ERROR_UNKNOWN = "unknown" export const ERROR_UNKNOWN = "unknown"
export const noop = () => null export const noop = () => null
export const asArray = input => (Array.isArray(input) ? input : [input])
export const findProjectBy = prop => val => projects => { export const findProjectBy = prop => val => projects => {
if (!val) { if (!val) {
@ -28,7 +29,7 @@ export const findProjectBy = prop => val => projects => {
return compose( return compose(
find(pathEq(prop, val)), find(pathEq(prop, val)),
flatMap(get("options")) flatMap(get("options")),
)(projects) )(projects)
} }
@ -38,14 +39,14 @@ export const findProjectByValue = findProjectBy("value")
export const findTask = id => export const findTask = id =>
compose( compose(
find(pathEq("value", Number(id))), find(pathEq("value", Number(id))),
get("tasks") get("tasks"),
) )
function taskOptions(tasks) { function taskOptions(tasks) {
return tasks.map(({ id, name, billable }) => ({ return tasks.map(({ id, name, billable }) => ({
label: billable ? name : `(${name})`, label: billable ? name : `(${name})`,
value: id, value: id,
billable billable,
})) }))
} }
@ -55,30 +56,30 @@ export function projectOptions(projects) {
label: project.intern ? `(${project.name})` : project.name, label: project.intern ? `(${project.name})` : project.name,
identifier: project.identifier, identifier: project.identifier,
customerName: project.customer_name, customerName: project.customer_name,
tasks: taskOptions(project.tasks) tasks: taskOptions(project.tasks),
})) }))
} }
export const groupedProjectOptions = compose( export const groupedProjectOptions = compose(
map(([customerName, projects]) => ({ map(([customerName, projects]) => ({
label: customerName, label: customerName,
options: projectOptions(projects) options: projectOptions(projects),
})), })),
toPairs, toPairs,
groupBy("customer_name"), groupBy("customer_name"),
nilToArray nilToArray,
) )
export const serializeProps = attrs => export const serializeProps = attrs =>
compose( compose(
mapValues(JSON.stringify), mapValues(JSON.stringify),
pick(attrs) pick(attrs),
) )
export const parseProps = attrs => export const parseProps = attrs =>
compose( compose(
mapValues(JSON.parse), mapValues(JSON.parse),
pick(attrs) pick(attrs),
) )
export const trace = curry((tag, value) => { export const trace = curry((tag, value) => {
@ -90,8 +91,7 @@ export const trace = curry((tag, value) => {
export const weekStartsOn = 1 export const weekStartsOn = 1
export const formatDate = date => format(date, "YYYY-MM-DD") export const formatDate = date => format(date, "YYYY-MM-DD")
export const extensionSettingsUrl = () => export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`
`chrome://extensions/?id=${chrome.runtime.id}`
export const extractAndSetTag = changeset => { export const extractAndSetTag = changeset => {
let { description } = changeset let { description } = changeset
@ -102,6 +102,6 @@ export const extractAndSetTag = changeset => {
return { return {
...changeset, ...changeset,
description: description.replace(/^#\S+\s/, ""), description: description.replace(/^#\S+\s/, ""),
tag: match[1] tag: match[1],
} }
} }

View File

@ -4,7 +4,7 @@ import {
ERROR_UPGRADE_REQUIRED, ERROR_UPGRADE_REQUIRED,
ERROR_UNKNOWN, ERROR_UNKNOWN,
groupedProjectOptions, groupedProjectOptions,
weekStartsOn weekStartsOn,
} from "utils" } from "utils"
import { get, forEach, reject, isNil } from "lodash/fp" import { get, forEach, reject, isNil } from "lodash/fp"
import { startOfWeek, endOfWeek } from "date-fns" import { startOfWeek, endOfWeek } from "date-fns"
@ -32,8 +32,8 @@ export function tabUpdated(tab, { messenger, settings }) {
type: "showBubble", type: "showBubble",
payload: { payload: {
bookedHours: parseFloat(data[0]?.hours) || 0, bookedHours: parseFloat(data[0]?.hours) || 0,
service service,
} },
}) })
}) })
.catch(() => { .catch(() => {
@ -41,8 +41,8 @@ export function tabUpdated(tab, { messenger, settings }) {
type: "showBubble", type: "showBubble",
payload: { payload: {
bookedHours: 0, bookedHours: 0,
service service,
} },
}) })
}) })
}) })
@ -58,7 +58,7 @@ export function settingsChanged(settings, { messenger }) {
forEach(tab => { forEach(tab => {
messenger.postMessage(tab, { type: "closePopup" }) messenger.postMessage(tab, { type: "closePopup" })
tabUpdated(tab, { settings, messenger }) tabUpdated(tab, { settings, messenger })
}) }),
) )
} }
@ -88,7 +88,7 @@ async function openPopup(tab, { service, messenger }) {
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),
]) ])
const action = { const action = {
type: "openPopup", type: "openPopup",
@ -103,8 +103,8 @@ async function openPopup(tab, { service, messenger }) {
schedules: get("[3].data", responses), schedules: get("[3].data", responses),
fromDate, fromDate,
toDate, toDate,
loading: false loading: false,
} },
} }
messenger.postMessage(tab, action) messenger.postMessage(tab, action)
} catch (error) { } catch (error) {
@ -119,7 +119,7 @@ async function openPopup(tab, { service, messenger }) {
} }
messenger.postMessage(tab, { messenger.postMessage(tab, {
type: "openPopup", type: "openPopup",
payload: { errorType, errorMessage } payload: { errorType, errorMessage },
}) })
} }
} }

View File

@ -1,17 +1,28 @@
import UrlPattern from "url-pattern" import UrlPattern from "url-pattern"
import { import { isFunction, isUndefined, compose, toPairs, map, pipe, isNil } from "lodash/fp"
isFunction, import { asArray } from "./index"
isUndefined,
compose,
toPairs,
map,
pipe
} from "lodash/fp"
import queryString from "query-string" import queryString from "query-string"
const extractQueryParams = (queryParams, query) => { function parseUrl(url) {
return toPairs(queryParams).reduce((acc, [key, param]) => { const urlObject = new URL(url)
acc[key] = query[param] const { origin, pathname, search } = urlObject
let { hash } = urlObject
const query = {
...queryString.parse(search),
...queryString.parse(hash),
}
if (hash) {
hash = hash.match(/#[^&]+/)[0]
}
return { origin, pathname, hash, query }
}
function extractQueryParams(queryParams, query) {
return toPairs(queryParams).reduce((acc, [key, params]) => {
const param = asArray(params).find(param => !isNil(query[param]))
if (param) {
acc[key] = query[param]
}
return acc return acc
}, {}) }, {})
} }
@ -37,9 +48,9 @@ const parseServices = compose(
return new UrlPattern(...pattern) return new UrlPattern(...pattern)
} }
return new UrlPattern(pattern) return new UrlPattern(pattern)
}) }),
})), })),
toPairs toPairs,
) )
export const createEnhancer = document => service => { export const createEnhancer = document => service => {
@ -57,19 +68,16 @@ export const createEnhancer = document => service => {
description: evaluate(service.description), description: evaluate(service.description),
projectId: evaluate(service.projectId), projectId: evaluate(service.projectId),
taskId: evaluate(service.taskId), taskId: evaluate(service.taskId),
position: service.position || { right: "calc(2rem + 5px)" } position: service.position || { right: "calc(2rem + 5px)" },
} }
} }
export const createMatcher = remoteServices => { export const createMatcher = remoteServices => {
const services = parseServices(remoteServices) const services = parseServices(remoteServices)
return tabUrl => { return tabUrl => {
const { origin, pathname, hash, search } = new URL(tabUrl) const { origin, pathname, hash, query } = parseUrl(tabUrl)
const url = `${origin}${pathname}${hash}` const url = `${origin}${pathname}${hash}`
const query = queryString.parse(search) const service = services.find(service => service.patterns.some(pattern => pattern.match(url)))
const service = services.find(service =>
service.patterns.some(pattern => pattern.match(url))
)
if (!service) { if (!service) {
return return
@ -78,10 +86,7 @@ export const createMatcher = remoteServices => {
const pattern = service.patterns.find(pattern => pattern.match(url)) const pattern = service.patterns.find(pattern => pattern.match(url))
let match = pattern.match(url) let match = pattern.match(url)
if (service.queryParams) { if (service.queryParams) {
const extractedQueryParams = extractQueryParams( const extractedQueryParams = extractQueryParams(service.queryParams, query)
service.queryParams,
query
)
match = { ...extractedQueryParams, ...match } match = { ...extractedQueryParams, ...match }
} }
@ -89,7 +94,7 @@ export const createMatcher = remoteServices => {
...match, ...match,
...service, ...service,
url: tabUrl, url: tabUrl,
match match,
} }
} }
} }
@ -99,6 +104,6 @@ export const createServiceFinder = remoteServices => document => {
const enhancer = createEnhancer(document) const enhancer = createEnhancer(document)
return pipe( return pipe(
matcher, matcher,
enhancer enhancer,
) )
} }

View File

@ -11,16 +11,14 @@ describe("utils", () => {
describe("createMatcher", () => { describe("createMatcher", () => {
it("matches host and path", () => { it("matches host and path", () => {
const service = matcher( const service = matcher("https://github.com/hundertzehn/mocoapp/pull/123")
"https://github.com/hundertzehn/mocoapp/pull/123"
)
expect(service.key).toEqual("github-pr") expect(service.key).toEqual("github-pr")
expect(service.name).toEqual("github") expect(service.name).toEqual("github")
}) })
it("matches query string", () => { it("matches query string", () => {
let service = matcher( let service = matcher(
"https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail&selectedIssue=TEST1-1" "https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail&selectedIssue=TEST1-1",
) )
expect(service.key).toEqual("jira") expect(service.key).toEqual("jira")
expect(service.name).toEqual("jira") expect(service.name).toEqual("jira")
@ -32,7 +30,7 @@ describe("utils", () => {
expect(service.match.id).toEqual("TEST1-1") expect(service.match.id).toEqual("TEST1-1")
service = matcher( service = matcher(
"https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail" "https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail",
) )
expect(service.key).toEqual("jira") expect(service.key).toEqual("jira")
expect(service.name).toEqual("jira") expect(service.name).toEqual("jira")
@ -44,7 +42,7 @@ describe("utils", () => {
expect(service.match.id).toBeUndefined() expect(service.match.id).toBeUndefined()
service = matcher( service = matcher(
"https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail&selectedIssue=" "https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&projectKey=TEST1&modal=detail&selectedIssue=",
) )
expect(service.key).toEqual("jira") expect(service.key).toEqual("jira")
expect(service.name).toEqual("jira") expect(service.name).toEqual("jira")
@ -55,9 +53,7 @@ describe("utils", () => {
expect(service.match.projectId).toEqual("TEST1") expect(service.match.projectId).toEqual("TEST1")
expect(service.match.id).toEqual("") expect(service.match.id).toEqual("")
service = matcher( service = matcher("https://moco-bx.atlassian.net/secure/RapidBoard.jspa")
"https://moco-bx.atlassian.net/secure/RapidBoard.jspa"
)
expect(service.key).toEqual("jira") expect(service.key).toEqual("jira")
expect(service.name).toEqual("jira") expect(service.name).toEqual("jira")
expect(service.match.org).toEqual("moco-bx") expect(service.match.org).toEqual("moco-bx")
@ -65,7 +61,7 @@ describe("utils", () => {
expect(service.match.id).toBeUndefined() expect(service.match.id).toBeUndefined()
service = matcher( service = matcher(
"https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&modal=detail&selectedIssue=TEST2-1" "https://moco-bx.atlassian.net/secure/RapidBoard.jspa?rapidView=2&modal=detail&selectedIssue=TEST2-1",
) )
expect(service.key).toEqual("jira") expect(service.key).toEqual("jira")
expect(service.name).toEqual("jira") expect(service.name).toEqual("jira")
@ -78,29 +74,47 @@ describe("utils", () => {
}) })
it("matches url with hash", () => { it("matches url with hash", () => {
let service = matcher( let service = matcher("https://www.wunderlist.com/webapp#/tasks/4771178545")
"https://www.wunderlist.com/webapp#/tasks/4771178545"
)
expect(service.key).toEqual("wunderlist") expect(service.key).toEqual("wunderlist")
expect(service.name).toEqual("wunderlist") expect(service.name).toEqual("wunderlist")
expect(service.match.id).toEqual("4771178545") expect(service.match.id).toEqual("4771178545")
}) })
it("does not match different host", () => { it("does not match different host", () => {
const service = matcher( const service = matcher("https://trello.com/hundertzehn/mocoapp/pull/123")
"https://trello.com/hundertzehn/mocoapp/pull/123"
)
expect(service).toBeFalsy() expect(service).toBeFalsy()
}) })
it("matches query string in the hash", () => {
const service = matcher(
"https://www.wrike.com/workspace.htm?acc=2771711#path=folder&id=342769537&p=342762920&a=2771711&c=board&ot=342769562&so=10&bso=10&sd=0&st=space-342762920",
)
expect(service.key).toEqual("wrike")
expect(service.name).toEqual("wrike")
expect(service.id).toEqual("342769562")
expect(service.match.id).toEqual("342769562")
})
it("matches query parameter with different names", () => {
expect(
matcher(
"https://www.wrike.com/workspace.htm?acc=2771711#path=mywork&id=342769537&p=342762920&a=2771711&c=board&ot=1234&so=10&bso=10&sd=0&st=space-342762920",
).id,
).toEqual("1234")
expect(
matcher(
"https://www.wrike.com/workspace.htm?acc=2771711#path=folder&id=342769537&p=342762920&a=2771711&c=board&t=1234&so=10&bso=10&sd=0&st=space-342762920",
).id,
).toEqual("1234")
})
}) })
describe("createEnhancer", () => { describe("createEnhancer", () => {
it("enhances a services", () => { it("enhances a services", () => {
const url = "https://github.com/hundertzehn/mocoapp/pull/123" const url = "https://github.com/hundertzehn/mocoapp/pull/123"
const document = { const document = {
querySelector: jest querySelector: jest.fn().mockReturnValue({ textContent: "[4321] Foo" }),
.fn()
.mockReturnValue({ textContent: "[4321] Foo" })
} }
const service = matcher(url) const service = matcher(url)
const enhancedService = createEnhancer(document)(service) const enhancedService = createEnhancer(document)(service)