From a9d17267070db7b1f30021052fd143c424fc75f0 Mon Sep 17 00:00:00 2001 From: Manuel Bouza Date: Fri, 26 Apr 2019 13:05:14 +0200 Subject: [PATCH] feature/wrike (#17) * Fix code styles * Add support for WRIKE * Add tests --- CHANGELOG.md | 6 ++++ package.json | 2 +- src/css/content.scss | 4 +-- src/js/background.js | 19 +++--------- src/js/remoteServices.js | 19 ++++++++++++ src/js/utils/index.js | 24 +++++++------- src/js/utils/messageHandlers.js | 20 ++++++------ src/js/utils/urlMatcher.js | 55 ++++++++++++++++++--------------- test/utils/urlMatcher.test.js | 52 +++++++++++++++++++------------ 9 files changed, 118 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e83200c..e01d8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 ### Fixed diff --git a/package.json b/package.json index 892d444..95e5b29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "moco-browser-extensions", "description": "Browser plugin for MOCO", - "version": "1.1.5", + "version": "1.2.0", "license": "MIT", "scripts": { "start": "yarn start:chrome", diff --git a/src/css/content.scss b/src/css/content.scss index 5ef29a0..aad519e 100644 --- a/src/css/content.scss +++ b/src/css/content.scss @@ -14,8 +14,7 @@ width: 60px; background-color: white; border-radius: 50%; - box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05), - 2px 2px 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); padding: 5px; z-index: 9999; @@ -69,6 +68,7 @@ height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.4); + z-index: 9999; .moco-bx-popup-content { background-color: white; diff --git a/src/js/background.js b/src/js/background.js index 66378ea..3eb9617 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,17 +1,8 @@ import "@babel/polyfill" import ApiClient from "api/Client" -import { - isChrome, - getCurrentTab, - getSettings, - isBrowserTab -} from "utils/browser" +import { isChrome, getCurrentTab, getSettings, isBrowserTab } from "utils/browser" import { BackgroundMessenger } from "utils/messaging" -import { - tabUpdated, - settingsChanged, - togglePopup -} from "utils/messageHandlers" +import { tabUpdated, settingsChanged, togglePopup } from "utils/messageHandlers" const messenger = new BackgroundMessenger() @@ -47,8 +38,8 @@ chrome.runtime.onMessage.addListener(action => { type: "showBubble", payload: { bookedHours: parseFloat(data[0]?.hours) || 0, - service - } + service, + }, }) }) }) @@ -56,7 +47,7 @@ chrome.runtime.onMessage.addListener(action => { if (error.response?.status === 422) { chrome.runtime.sendMessage({ type: "setFormErrors", - payload: error.response.data + payload: error.response.data, }) } }) diff --git a/src/js/remoteServices.js b/src/js/remoteServices.js index e230e0a..309f6b3 100644 --- a/src/js/remoteServices.js +++ b/src/js/remoteServices.js @@ -95,6 +95,25 @@ export default { 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: { name: "wunderlist", urlPatterns: ["https\\://www.wunderlist.com/(webapp)#/tasks/:id(/*)"], diff --git a/src/js/utils/index.js b/src/js/utils/index.js index d0d0379..ffc5f63 100644 --- a/src/js/utils/index.js +++ b/src/js/utils/index.js @@ -9,7 +9,7 @@ import { get, find, curry, - pick + pick, } from "lodash/fp" import { format } from "date-fns" @@ -20,6 +20,7 @@ export const ERROR_UPGRADE_REQUIRED = "upgrade-required" export const ERROR_UNKNOWN = "unknown" export const noop = () => null +export const asArray = input => (Array.isArray(input) ? input : [input]) export const findProjectBy = prop => val => projects => { if (!val) { @@ -28,7 +29,7 @@ export const findProjectBy = prop => val => projects => { return compose( find(pathEq(prop, val)), - flatMap(get("options")) + flatMap(get("options")), )(projects) } @@ -38,14 +39,14 @@ export const findProjectByValue = findProjectBy("value") export const findTask = id => compose( find(pathEq("value", Number(id))), - get("tasks") + get("tasks"), ) function taskOptions(tasks) { return tasks.map(({ id, name, billable }) => ({ label: billable ? name : `(${name})`, value: id, - billable + billable, })) } @@ -55,30 +56,30 @@ export function projectOptions(projects) { label: project.intern ? `(${project.name})` : project.name, identifier: project.identifier, customerName: project.customer_name, - tasks: taskOptions(project.tasks) + tasks: taskOptions(project.tasks), })) } export const groupedProjectOptions = compose( map(([customerName, projects]) => ({ label: customerName, - options: projectOptions(projects) + options: projectOptions(projects), })), toPairs, groupBy("customer_name"), - nilToArray + nilToArray, ) export const serializeProps = attrs => compose( mapValues(JSON.stringify), - pick(attrs) + pick(attrs), ) export const parseProps = attrs => compose( mapValues(JSON.parse), - pick(attrs) + pick(attrs), ) export const trace = curry((tag, value) => { @@ -90,8 +91,7 @@ export const trace = curry((tag, value) => { export const weekStartsOn = 1 export const formatDate = date => format(date, "YYYY-MM-DD") -export const extensionSettingsUrl = () => - `chrome://extensions/?id=${chrome.runtime.id}` +export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}` export const extractAndSetTag = changeset => { let { description } = changeset @@ -102,6 +102,6 @@ export const extractAndSetTag = changeset => { return { ...changeset, description: description.replace(/^#\S+\s/, ""), - tag: match[1] + tag: match[1], } } diff --git a/src/js/utils/messageHandlers.js b/src/js/utils/messageHandlers.js index 6a8bc13..026d184 100644 --- a/src/js/utils/messageHandlers.js +++ b/src/js/utils/messageHandlers.js @@ -4,7 +4,7 @@ import { ERROR_UPGRADE_REQUIRED, ERROR_UNKNOWN, groupedProjectOptions, - weekStartsOn + weekStartsOn, } from "utils" import { get, forEach, reject, isNil } from "lodash/fp" import { startOfWeek, endOfWeek } from "date-fns" @@ -32,8 +32,8 @@ export function tabUpdated(tab, { messenger, settings }) { type: "showBubble", payload: { bookedHours: parseFloat(data[0]?.hours) || 0, - service - } + service, + }, }) }) .catch(() => { @@ -41,8 +41,8 @@ export function tabUpdated(tab, { messenger, settings }) { type: "showBubble", payload: { bookedHours: 0, - service - } + service, + }, }) }) }) @@ -58,7 +58,7 @@ export function settingsChanged(settings, { messenger }) { forEach(tab => { messenger.postMessage(tab, { type: "closePopup" }) tabUpdated(tab, { settings, messenger }) - }) + }), ) } @@ -88,7 +88,7 @@ async function openPopup(tab, { service, messenger }) { apiClient.login(service), apiClient.projects(), apiClient.activities(fromDate, toDate), - apiClient.schedules(fromDate, toDate) + apiClient.schedules(fromDate, toDate), ]) const action = { type: "openPopup", @@ -103,8 +103,8 @@ async function openPopup(tab, { service, messenger }) { schedules: get("[3].data", responses), fromDate, toDate, - loading: false - } + loading: false, + }, } messenger.postMessage(tab, action) } catch (error) { @@ -119,7 +119,7 @@ async function openPopup(tab, { service, messenger }) { } messenger.postMessage(tab, { type: "openPopup", - payload: { errorType, errorMessage } + payload: { errorType, errorMessage }, }) } } diff --git a/src/js/utils/urlMatcher.js b/src/js/utils/urlMatcher.js index dadf007..4767fa2 100644 --- a/src/js/utils/urlMatcher.js +++ b/src/js/utils/urlMatcher.js @@ -1,17 +1,28 @@ import UrlPattern from "url-pattern" -import { - isFunction, - isUndefined, - compose, - toPairs, - map, - pipe -} from "lodash/fp" +import { isFunction, isUndefined, compose, toPairs, map, pipe, isNil } from "lodash/fp" +import { asArray } from "./index" import queryString from "query-string" -const extractQueryParams = (queryParams, query) => { - return toPairs(queryParams).reduce((acc, [key, param]) => { - acc[key] = query[param] +function parseUrl(url) { + const urlObject = new URL(url) + 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 }, {}) } @@ -37,9 +48,9 @@ const parseServices = compose( return new UrlPattern(...pattern) } return new UrlPattern(pattern) - }) + }), })), - toPairs + toPairs, ) export const createEnhancer = document => service => { @@ -57,19 +68,16 @@ export const createEnhancer = document => service => { description: evaluate(service.description), projectId: evaluate(service.projectId), taskId: evaluate(service.taskId), - position: service.position || { right: "calc(2rem + 5px)" } + position: service.position || { right: "calc(2rem + 5px)" }, } } export const createMatcher = remoteServices => { const services = parseServices(remoteServices) return tabUrl => { - const { origin, pathname, hash, search } = new URL(tabUrl) + const { origin, pathname, hash, query } = parseUrl(tabUrl) 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) { return @@ -78,10 +86,7 @@ export const createMatcher = remoteServices => { const pattern = service.patterns.find(pattern => pattern.match(url)) let match = pattern.match(url) if (service.queryParams) { - const extractedQueryParams = extractQueryParams( - service.queryParams, - query - ) + const extractedQueryParams = extractQueryParams(service.queryParams, query) match = { ...extractedQueryParams, ...match } } @@ -89,7 +94,7 @@ export const createMatcher = remoteServices => { ...match, ...service, url: tabUrl, - match + match, } } } @@ -99,6 +104,6 @@ export const createServiceFinder = remoteServices => document => { const enhancer = createEnhancer(document) return pipe( matcher, - enhancer + enhancer, ) } diff --git a/test/utils/urlMatcher.test.js b/test/utils/urlMatcher.test.js index 2402a30..3d00d78 100644 --- a/test/utils/urlMatcher.test.js +++ b/test/utils/urlMatcher.test.js @@ -11,16 +11,14 @@ describe("utils", () => { describe("createMatcher", () => { it("matches host and path", () => { - const service = matcher( - "https://github.com/hundertzehn/mocoapp/pull/123" - ) + const service = matcher("https://github.com/hundertzehn/mocoapp/pull/123") expect(service.key).toEqual("github-pr") expect(service.name).toEqual("github") }) it("matches query string", () => { 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.name).toEqual("jira") @@ -32,7 +30,7 @@ describe("utils", () => { expect(service.match.id).toEqual("TEST1-1") 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.name).toEqual("jira") @@ -44,7 +42,7 @@ describe("utils", () => { expect(service.match.id).toBeUndefined() 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.name).toEqual("jira") @@ -55,9 +53,7 @@ describe("utils", () => { expect(service.match.projectId).toEqual("TEST1") expect(service.match.id).toEqual("") - service = matcher( - "https://moco-bx.atlassian.net/secure/RapidBoard.jspa" - ) + service = matcher("https://moco-bx.atlassian.net/secure/RapidBoard.jspa") expect(service.key).toEqual("jira") expect(service.name).toEqual("jira") expect(service.match.org).toEqual("moco-bx") @@ -65,7 +61,7 @@ describe("utils", () => { expect(service.match.id).toBeUndefined() 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.name).toEqual("jira") @@ -78,29 +74,47 @@ describe("utils", () => { }) it("matches url with hash", () => { - let service = matcher( - "https://www.wunderlist.com/webapp#/tasks/4771178545" - ) + let service = matcher("https://www.wunderlist.com/webapp#/tasks/4771178545") expect(service.key).toEqual("wunderlist") expect(service.name).toEqual("wunderlist") expect(service.match.id).toEqual("4771178545") }) it("does not match different host", () => { - const service = matcher( - "https://trello.com/hundertzehn/mocoapp/pull/123" - ) + const service = matcher("https://trello.com/hundertzehn/mocoapp/pull/123") 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", () => { it("enhances a services", () => { const url = "https://github.com/hundertzehn/mocoapp/pull/123" const document = { - querySelector: jest - .fn() - .mockReturnValue({ textContent: "[4321] Foo" }) + querySelector: jest.fn().mockReturnValue({ textContent: "[4321] Foo" }), } const service = matcher(url) const enhancedService = createEnhancer(document)(service)