Compare commits

...

8 Commits

Author SHA1 Message Date
Manuel Bouza
b02be37bdd Update CHANGELOG 2019-03-26 16:44:12 +01:00
Manuel Bouza
76422d7343 Update configuration for local builds 2019-03-26 16:13:27 +01:00
Manuel Bouza
22ac8f4984 Update README, add CHANGELOG 2019-03-26 16:13:27 +01:00
Manuel Bouza
1b1fae6f7a Allow to set tag in description 2019-03-26 16:00:02 +01:00
Manuel Bouza
f49c0bdc3d Update jira service configuratioin 2019-03-26 12:41:00 +01:00
Manuel Bouza
b9f417140d Create source zip after build 2019-03-26 12:40:19 +01:00
Manuel Bouza
29db681e1c Set default Bubble position to bottom right. 2019-03-26 10:01:58 +01:00
Manuel Bouza
dda92746fa Set subdomain to "__unset__" if it empty
This is to prevent invalid network requests with an empty subdomain.
2019-03-26 10:01:37 +01:00
16 changed files with 248 additions and 57 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
BUGSNAG_API_KEY=
APPLICATION_ID=

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules/ node_modules/
build/ build/
.env

30
CHANGELOG.md Normal file
View File

@@ -0,0 +1,30 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- Add support for starting/stopping a timer
- Show hours as HH:MM or decimal in the Bubble, depending on setting in MOCO
## [1.0.18] - 2019-03-23
### Added
- First release of version 1
## [1.0.19] - 2019-03-26
### Changed
- Position Bubble in the bottom right by default
### Fixed
- Set default value of subdomain to `__unset__` to prevent network error if it is empty
## [1.0.20] - 2019-03-26
### Added
- Add support for tags in description
## [1.0.21] - 2019-03-26
### Changed
- Update README with example configuration and instructions for local installation

116
README.md
View File

@@ -1,15 +1,6 @@
mocoapp-browser-extension # MOCO Browser Extension
=========================
Documentation ## Development
-------------
* https://checklyhq.com/blog/2018/08/creating-a-chrome-extension-in-2018-the-good-the-bad-and-the-meh/
* https://developer.chrome.com/extensions
* https://developer.chrome.com/extensions/api_index
Development
-----------
* run `yarn` * run `yarn`
* run `yarn start:chrome` or `yarn start:firefox` (`yarn start` is an alias for `yarn start:chrome`) * run `yarn start:chrome` or `yarn start:firefox` (`yarn start` is an alias for `yarn start:chrome`)
@@ -18,9 +9,106 @@ Development
* Firefox: visit `about:debugging` and load temporary Add-on from `build/firefox` * Firefox: visit `about:debugging` and load temporary Add-on from `build/firefox`
* reload browser extension after change * reload browser extension after change
Release ## Production Build
-------
* bump version in `package.json` * bump version in `package.json`
* run `yarn build` * run `yarn build`
* upload Chrome and Firefox extensions in `build/chrome` and `build/firefox` respectively * The Chrome and Firefox extensions are available as ZIP-files in `build/chrome` and `build/firefox` respectively
## Install Local Builds
### Chrome
1. `yarn build:chrome`
1. Visit `chrome://extensions`
2. Enable `Developer mode`
3. `Load unpacked` and select the `build/chrome` folder.
### Firefox
1. `yarn build:firefox`
1. Visit `about:debugging`
2. Click on `Load temporary Add-on` and select the ZIP-file in `build/firefox`
Only signed extensions can be permantly installed in Firefox (unless you are using <em>Firefox Developer Edition</em>). To sign the build, proceed as described in [Getting started with web-ext](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Getting_started_with_web-ext).
You can keep the extension settings between builds by providing a stable `APPLICATION_ID` between builds. You can set an `APPLICATION_ID` in a file named `.env` or at build time as follows:
`APPLICATION_ID=my-custom-moco-extension@mycompany.com yarn build:firefox`
## Remote Service Configuration
Remote services are configured in `src/remoteServices.js`.
A remote service is configured as follows:
```javascript
{
service_key: {
name: "service_name",
urlPatterns: [
"https:\\://:subdomain.example.com/card/:id",
[/^https:\/\/(\w+).example.com\/card\/(\d+), ["subdomain", "id"]],
],
queryParams: {
projectId: "currentList"
},
description: (document, service, { subdomain, id, projectId }) => {
const title = document
.querySelector('.title')
?.textContent
?.trim()
return `#${id} ${service.key} ${title || ""}`
},
projectId: (document, service, { subdomain, id, projectId }) => {
return projectId
},
position: { left: "50%", transform: "translate(-50%)" }
}
}
```
| Parameter | Description |
|--------------|:-------------|
| service_key | `string` &mdash; Unique identifier for the service |
| service_name | `string` &mdash; Must be one of the registered services `trello`, `jira`, `asana`, `wunderlist`, `github` or `youtrack` |
| urlPatterns | `string` \| `RegEx` &mdash; A valid URL pattern or regular expression, as described in the [url-pattern](https://www.npmjs.com/package/url-pattern) package. |
| queryParams | `Object` &mdash; The object value is the name of the query parameter and the key the property it will available on, e.g. the value of the query parameter `currentList` will be available under `projectId`. Matches in `urlPatterns` have precedence over matches in `queryParams`. |
| description | `undefined` \| `string` \| `function` &mdash; The default description of the service. If it is a function, it will receive `window.document`, the current `service` and an object with the URL `matches` as arguments, and the return value set as the default description. |
| projectId | `undefined` \| `string` \| `function` &mdash; The pre-selected project of the service matching the MOCO project identifier. If it is a function, it will receive `window.document`, the current `service` and an object with the URL `matches` as arguments, and must return the MOCO project identifier or `undefined`. |
| position | `Object` &mdash; CSS properties as object styles for position the bubble. Defaults to `{ right: calc(4rem + 5px)` |
## Adding a Custom Service
1. Fork and clone this repository
2. Add your service to `src/removeServices.js`, e.g. for self-hosted Jira copy the entry with the key `jira` and update the `urlPatterns`:
```javascript
"self-hosted-jira": {
name: "jira",
urlPatterns: [
"https\\://jira.my-company.com/secure/RapidBoard.jspa",
"https\\://jira.my-company.net/browse/:id",
"https\\://jira.my-company.net/jira/software/projects/:projectId/boards/:board",
"https\\://jira.my-company.net/jira/software/projects/:projectId/boards/:board/backlog"
],
queryParams: {
id: "selectedIssue",
projectId: "projectKey"
},
description: (document, service, { id }) => {
const title =
document
.querySelector('[aria-label="Edit Summary"]')
?.parentNode?.querySelector("h1")
?.textContent?.trim() ||
document
.querySelector(".ghx-selected .ghx-summary")
?.textContent?.trim()
return `#${id} ${title || ""}`
}
},
```
3. Build the extension (see [Production Build](#production-build)).
4. Install the extension locally (see [Install Local Builds](#install-local-builds)).

View File

@@ -1,17 +1,18 @@
{ {
"name": "moco-browser-extensions", "name": "moco-browser-extensions",
"description": "Browser plugin for MOCO", "description": "Browser plugin for MOCO",
"version": "1.0.18", "version": "1.0.21",
"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",
"start:firefox": "node_modules/.bin/webpack --config webpack.firefox.config.js --watch --env.browser firefox --env.NODE_ENV development", "start:firefox": "node_modules/.bin/webpack --config webpack.firefox.config.js --watch --env.browser firefox --env.NODE_ENV development",
"zip:chrome": "zip -qr build/chrome/moco-bx-source.zip . -x .git/\\* build/\\* node_modules/\\* test/\\* .DS_Store",
"zip:firefox": "zip -qr build/firefox/moco-bx-source.zip . -x .git/\\* build/\\* node_modules/\\* test/\\* .DS_Store",
"build:chrome": "node_modules/.bin/webpack -p --config webpack.chrome.config.js --env.browser chrome --env.NODE_ENV production", "build:chrome": "node_modules/.bin/webpack -p --config webpack.chrome.config.js --env.browser chrome --env.NODE_ENV production",
"build:firefox": "node_modules/.bin/webpack -p --config webpack.firefox.config.js --env.browser firefox --env.NODE_ENV production", "build:firefox": "node_modules/.bin/webpack -p --config webpack.firefox.config.js --env.browser firefox --env.NODE_ENV production",
"build": "yarn run build:firefox && yarn run build:chrome", "build": "yarn build:firefox && yarn zip:firefox && yarn build:chrome && yarn zip:chrome",
"test": "node_modules/.bin/jest", "test": "node_modules/.bin/jest",
"test:watch": "node_modules/.bin/jest --watch", "test:watch": "node_modules/.bin/jest --watch"
"release": "copyfiles main.css main.min.js background.min.js manifest.json popup.html options.html node_modules/jquery/dist/jquery.min.js node_modules/select2/select2.js src/images/* release"
}, },
"dependencies": { "dependencies": {
"@bugsnag/js": "^5.2.0", "@bugsnag/js": "^5.2.0",
@@ -19,6 +20,7 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"date-fns": "^1.30.1", "date-fns": "^1.30.1",
"dotenv": "^7.0.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"mobx": "^5.5.0", "mobx": "^5.5.0",
"mobx-react": "^5.2.8", "mobx-react": "^5.2.8",
@@ -55,6 +57,7 @@
"prettier": "^1.16.4", "prettier": "^1.16.4",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"uuid": "^3.3.2",
"webpack": "^4.15.0", "webpack": "^4.15.0",
"webpack-bugsnag-plugins": "^1.3.0", "webpack-bugsnag-plugins": "^1.3.0",
"webpack-cli": "^3.0.8", "webpack-cli": "^3.0.8",

View File

@@ -10,6 +10,7 @@ import {
ERROR_UNKNOWN, ERROR_UNKNOWN,
ERROR_UNAUTHORIZED, ERROR_UNAUTHORIZED,
ERROR_UPGRADE_REQUIRED, ERROR_UPGRADE_REQUIRED,
extractAndSetTag,
findProjectByValue, findProjectByValue,
findProjectByIdentifier, findProjectByIdentifier,
findTask, findTask,
@@ -80,13 +81,11 @@ class App extends Component {
seconds: seconds:
this.changeset.hours && this.changeset.hours &&
new TimeInputParser(this.changeset.hours).parseSeconds(), new TimeInputParser(this.changeset.hours).parseSeconds(),
description: service?.description description: service?.description,
tag: ""
} }
return { return { ...defaults, ...this.changeset }
...defaults,
...this.changeset
}
} }
componentDidMount() { componentDidMount() {
@@ -124,7 +123,7 @@ class App extends Component {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: "createActivity", type: "createActivity",
payload: { payload: {
activity: this.changesetWithDefaults, activity: extractAndSetTag(this.changesetWithDefaults),
service service
} }
}) })

View File

@@ -12,7 +12,7 @@ class Options extends Component {
@observable isSuccess = false; @observable isSuccess = false;
componentDidMount() { componentDidMount() {
getSettings().then(({ subdomain, apiKey }) => { getSettings(false).then(({ subdomain, apiKey }) => {
this.subdomain = subdomain || "" this.subdomain = subdomain || ""
this.apiKey = apiKey || "" this.apiKey = apiKey || ""
}) })

View File

@@ -31,8 +31,7 @@ export default {
?.textContent.trim() ?.textContent.trim()
?.match(/^\[(\d+)\]/) ?.match(/^\[(\d+)\]/)
return match && match[1] return match && match[1]
}, }
position: { right: "2rem" }
}, },
"github-issue": { "github-issue": {
@@ -41,8 +40,7 @@ export default {
id: (document, service, { org, repo, id }) => id: (document, service, { org, repo, id }) =>
[service.key, org, repo, id].join("."), [service.key, org, repo, id].join("."),
description: (document, service, { org, repo, id }) => description: (document, service, { org, repo, id }) =>
document.querySelector(".js-issue-title")?.textContent?.trim(), document.querySelector(".js-issue-title")?.textContent?.trim()
position: { right: "2rem" }
}, },
jira: { jira: {
@@ -66,7 +64,7 @@ export default {
document document
.querySelector(".ghx-selected .ghx-summary") .querySelector(".ghx-selected .ghx-summary")
?.textContent?.trim() ?.textContent?.trim()
return `[${id}] ${title || ""}` return `#${id} ${title || ""}`
} }
}, },
@@ -74,8 +72,7 @@ export default {
name: "trello", name: "trello",
urlPatterns: ["https\\://trello.com/c/:id/:title"], urlPatterns: ["https\\://trello.com/c/:id/:title"],
description: (document, service, { title }) => description: (document, service, { title }) =>
document.querySelector(".js-title-helper")?.textContent?.trim() || title, document.querySelector(".js-title-helper")?.textContent?.trim() || title
position: { right: "calc(2rem + 4px)" }
}, },
youtrack: { youtrack: {
@@ -91,7 +88,6 @@ export default {
description: document => description: document =>
document document
.querySelector(".taskItem.selected .taskItem-titleWrapper-title") .querySelector(".taskItem.selected .taskItem-titleWrapper-title")
?.textContent?.trim(), ?.textContent?.trim()
position: { right: "calc(2rem + 4px)" }
} }
} }

View File

@@ -1,18 +1,26 @@
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
import { head } from "lodash/fp"
export const getSettings = () => { export const getSettings = (withDefaultSubdomain = true) => {
const keys = ["subdomain", "apiKey"] const keys = ["subdomain", "apiKey"]
const { version } = chrome.runtime.getManifest() const { version } = chrome.runtime.getManifest()
if (isChrome()) { if (isChrome()) {
return new Promise(resolve => { return new Promise(resolve => {
chrome.storage.sync.get(keys, data => { chrome.storage.sync.get(keys, data => {
if (withDefaultSubdomain) {
data.subdomain = data.subdomain || "__unset__"
}
resolve({ ...data, version }) resolve({ ...data, version })
}) })
}) })
} else { } else {
return browser.storage.sync.get(keys).then(data => ({ ...data, version })) return browser.storage.sync.get(keys).then(data => {
if (withDefaultSubdomain) {
data.subdomain = data.subdomain || "__unset__"
}
return { ...data, version }
})
} }
} }

View File

@@ -86,3 +86,16 @@ 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 => {
let { description } = changeset
const match = description.match(/^#(\S+)/)
if (!match) {
return changeset
}
return {
...changeset,
description: description.replace(/^#\S+\s/, ""),
tag: match[1]
}
}

View File

@@ -25,7 +25,7 @@ const filterReport = report => {
} }
const bugsnagClient = bugsnag({ const bugsnagClient = bugsnag({
apiKey: "da6caac4af70af3e4683454b40fe5ef5", apiKey: process.env.BUGSNAG_API_KEY,
appVersion: getAppVersion(), appVersion: getAppVersion(),
collectUserIp: false, collectUserIp: false,
beforeSend: filterReport, beforeSend: filterReport,

View File

@@ -57,7 +57,7 @@ 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 || { left: "50%", transform: "translateX(-50%)" } position: service.position || { right: "calc(2rem + 5px)" }
} }
} }

View File

@@ -3,7 +3,8 @@ import {
findProjectByValue, findProjectByValue,
findProjectByIdentifier, findProjectByIdentifier,
findTask, findTask,
groupedProjectOptions groupedProjectOptions,
extractAndSetTag
} from "../../src/js/utils" } from "../../src/js/utils"
import { map } from "lodash/fp" import { map } from "lodash/fp"
@@ -86,4 +87,36 @@ describe("utils", () => {
]) ])
}) })
}) })
describe("extractAndSetTag", () => {
it("sets the correct tag and updates description", () => {
const changeset = {
description: "#meeting Lorem ipsum",
tag: ""
}
expect(extractAndSetTag(changeset)).toEqual({
description: "Lorem ipsum",
tag: "meeting"
})
})
it("only matches tag at the beginning", () => {
const changeset = {
description: "Lorem #meeting ipsum",
tag: ""
}
expect(extractAndSetTag(changeset)).toEqual(changeset)
})
it("returns the changeset if not tag is set", () => {
const changeset = {
description: "Without tag",
tag: ""
}
expect(extractAndSetTag(changeset)).toEqual(changeset)
})
})
}) })

View File

@@ -1,3 +1,5 @@
require("dotenv").config()
const path = require("path") const path = require("path")
const webpack = require("webpack") const webpack = require("webpack")
const CleanWebpackPlugin = require("clean-webpack-plugin") const CleanWebpackPlugin = require("clean-webpack-plugin")
@@ -60,7 +62,10 @@ module.exports = env => {
plugins: [ plugins: [
new CleanWebpackPlugin([`build/${env.browser}`]), new CleanWebpackPlugin([`build/${env.browser}`]),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV) "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
"process.env.BUGSNAG_API_KEY": JSON.stringify(
process.env.BUGSNAG_API_KEY
)
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].css", filename: "[name].css",
@@ -95,22 +100,27 @@ module.exports = env => {
if (env.NODE_ENV === "production") { if (env.NODE_ENV === "production") {
config.devtool = "source-maps" config.devtool = "source-maps"
if (process.env.BUGSNAG_API_KEY) {
config.plugins.push(
new BugsnagBuildReporterPlugin({
apiKey: process.env.BUGSNAG_API_KEY,
appVersion: process.env.npm_package_version,
releaseStage: "production"
}),
// important: upload sourcemaps before removing source mapping url
new BugsnagSourceMapUploaderPlugin({
apiKey: process.env.BUGSNAG_API_KEY,
appVersion: process.env.npm_package_version,
publicPath:
env.browser === "firefox"
? "moz-extension*://*/"
: "chrome-extension*://*/", // extra asterisk after protocol needed
overwrite: true
})
)
}
config.plugins.push( config.plugins.push(
new BugsnagBuildReporterPlugin({
apiKey: "da6caac4af70af3e4683454b40fe5ef5",
appVersion: process.env.npm_package_version,
releaseStage: "production"
}),
// important: upload sourcemaps before removing source mapping url
new BugsnagSourceMapUploaderPlugin({
apiKey: "da6caac4af70af3e4683454b40fe5ef5",
appVersion: process.env.npm_package_version,
publicPath:
env.browser === "firefox"
? "moz-extension*://*/"
: "chrome-extension*://*/", // extra asterisk after protocol needed
overwrite: true
}),
new RemoveSourceMapPlugin(), new RemoveSourceMapPlugin(),
new ZipPlugin({ new ZipPlugin({
filename: `moco-bx-${env.browser}-v${ filename: `moco-bx-${env.browser}-v${

View File

@@ -1,3 +1,4 @@
const uuidv4 = require("uuid/v4")
const CopyWebpackPlugin = require("copy-webpack-plugin") const CopyWebpackPlugin = require("copy-webpack-plugin")
const { compact } = require("lodash/fp") const { compact } = require("lodash/fp")
@@ -30,7 +31,9 @@ module.exports = env => {
browser_style: true browser_style: true
}, },
browser_specific_settings: { browser_specific_settings: {
gecko: { id: "browser-extension@mocoapp.com" } gecko: {
id: process.env.APPLICATION_ID || `{${uuidv4()}}`
}
}, },
description: process.env.npm_package_description, description: process.env.npm_package_description,
version: process.env.npm_package_version version: process.env.npm_package_version

View File

@@ -2437,6 +2437,11 @@ domutils@1.5.1:
dom-serializer "0" dom-serializer "0"
domelementtype "1" domelementtype "1"
dotenv@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"
integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==
duplexify@^3.4.2, duplexify@^3.6.0: duplexify@^3.4.2, duplexify@^3.6.0:
version "3.7.1" version "3.7.1"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"