From 28a9a86e2736ff97aba3e1ad4e7a6c9a0f0e8cbd Mon Sep 17 00:00:00 2001 From: Manuel Bouza Date: Fri, 22 Mar 2019 15:56:24 +0100 Subject: [PATCH] MOCO Browser Extension (#2) * spike * initial draft * updated styling * skeleton * added bubble script to webpack * added linter settings * installs * first implementation * Update webpack config - write bundle to `/build` - add support for SASS - improve options view as a proof o concept for styling * Update es-lint rules to mach mocoapp * Upgrade npm packages * Mount Bubble only for configured services * Update react and babel * Move module resolution config to webpack * Syncrhonize apiClient with chrome storage * Load projects and initialize form with last project and task * Enhance service * Improve handling of changeset with defaults * Create activity * Show error page on missing configuration * Refactor so that changeset can be used as activity params * Show form errors * Fetch and show booked hours for service * Allow to book hours with colon, error handling, spinner * WIP: Shadow DOM * Remove shadow dom * Render App in iframe * Refactor App component to load projects and create activity * Bugsnag integration * Add title to form and timer hint to hours input field * Configure positioning of bubble * Get rid of shared browser instance * Show Calendar and animate buble * Update webpack config * Prevent double animation of bubble * Fix eslint * Add margin to iframe body * Submit form when pressing enter on textarea * Open select on Enter * Use local environment for development * Show upgrade error if version invalid * Add asana service * Add jira and wunderlist services, add better support for query strings * Match urls with hash * Show popup in browser action * Pump version, add version to zip file * Add youtrack service * WIP: always show browserAction * Refactor * Update design * Finalize release 1.0.3 * Fix styles * Add support for Firefox browser * Extract common webpack config * Fix eslint * Close modal with ESC key * Use TimeInputParser to parse hours input * Improve webpack config * Show modal instead of popup when clicking on browser action * Pre-select last booked activities on service * Remove badge from booked hours * Show error and success feedback on options page * Remove updateBrowserActionForTab * Animate Bubble on unmount * Fix select date * Refactor * Fix key shortcut * Show schedule in calendar * Upload source maps to bugsnag * Upload sourcemaps to bugsnag * Define command shortcuts * Fix race condition where both Bubble and content wanted to mount Popup The content script is now the only place, where the Popup is mounted * Replace hash in filename by version * No new line in textarea and updated shortcuts for chrome * Change shortcut to Ctrl+Shift+K * Fix cors issue in new chrome 73 * Style improvements * Only report errors from own sources * Prevent sending messages to browser tabs * Fix scrollbars in iframe * Add error page for unknown error * Add stop propagation to Bubble click event * Update error pages * Remove timeout in tabHandler. The messaging error occurs only when the browser extension is reloaded/updated without refreshing the browser tab. * Refactor messaging * Show spinner in popup * Extract message handler to own module * Update styles and texts of error pages * Ensure focus is on document when opening popup * Find projects by identifier and value, do not highlight selected option in select component * Update docs * Spread match properties on service; improve remote service configuration for jira and wunderlist * Add webpack plugin to remove source mapping url * Bugsnag do not collect user ip * Upload source maps before removing source mapping url in bundles * Add support for regex url patterns, update asana config. * Fix animation Set default transform property via css * Improve config for asana * Change to fad-in/out animation --- .babelrc | 8 + .eslintrc.json | 73 + .gitignore | 2 + README.md | 26 + jest.config.js | 1 + manifest.json | 47 - package.json | 63 + src/background.html | 8 + src/css/_button.scss | 42 + src/css/_form.scss | 91 + src/css/_reset.scss | 43 + src/css/_spinner.scss | 24 + src/css/_variables.scss | 5 + src/css/content.scss | 77 + src/css/options.scss | 42 + src/css/popup.scss | 128 + src/images/error_general.png | Bin 0 -> 20549 bytes src/images/firefox_addons.png | Bin 0 -> 4814 bytes src/images/settings.png | Bin 0 -> 29178 bytes src/js/api/Client.js | 63 + src/js/background.js | 128 + src/js/components/App.js | 212 + src/js/components/Bubble.js | 23 + src/js/components/Calendar/Day.js | 40 + src/js/components/Calendar/Hours.js | 43 + src/js/components/Calendar/index.js | 60 + .../Errors/InvalidConfigurationError.js | 28 + src/js/components/Errors/UnknownError.js | 21 + .../components/Errors/UpgradeRequiredError.js | 35 + src/js/components/Form.js | 113 + src/js/components/Options.js | 103 + src/js/components/Popup.js | 87 + src/js/components/Select.js | 132 + src/js/components/Spinner.js | 14 + src/js/components/shared/Header.js | 13 + src/js/content.js | 115 + src/js/options.js | 14 + src/js/popup.js | 31 + src/js/remoteServices.js | 97 + src/js/utils/TimeInputParser.js | 91 + src/js/utils/browser.js | 41 + src/js/utils/index.js | 88 + src/js/utils/messageHandlers.js | 127 + src/js/utils/messaging.js | 99 + src/js/utils/notifier.js | 39 + src/js/utils/urlMatcher.js | 104 + src/manifest.json | 45 + src/options.html | 9 + src/popup.html | 9 + test/data.js | 108 + test/utils/TimeInputParser.test.js | 36 + test/utils/index.test.js | 89 + test/utils/urlMatcher.test.js | 114 + webpack.base.config.js | 125 + webpack.chrome.config.js | 42 + webpack.firefox.config.js | 45 + webpack/RemoveSourceMapPlugin.js | 23 + yarn.lock | 7778 +++++++++++++++++ 58 files changed, 11017 insertions(+), 47 deletions(-) create mode 100644 .babelrc create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100644 jest.config.js delete mode 100644 manifest.json create mode 100644 package.json create mode 100644 src/background.html create mode 100644 src/css/_button.scss create mode 100644 src/css/_form.scss create mode 100644 src/css/_reset.scss create mode 100644 src/css/_spinner.scss create mode 100644 src/css/_variables.scss create mode 100644 src/css/content.scss create mode 100644 src/css/options.scss create mode 100644 src/css/popup.scss create mode 100644 src/images/error_general.png create mode 100644 src/images/firefox_addons.png create mode 100644 src/images/settings.png create mode 100644 src/js/api/Client.js create mode 100644 src/js/background.js create mode 100644 src/js/components/App.js create mode 100644 src/js/components/Bubble.js create mode 100644 src/js/components/Calendar/Day.js create mode 100644 src/js/components/Calendar/Hours.js create mode 100644 src/js/components/Calendar/index.js create mode 100644 src/js/components/Errors/InvalidConfigurationError.js create mode 100644 src/js/components/Errors/UnknownError.js create mode 100644 src/js/components/Errors/UpgradeRequiredError.js create mode 100644 src/js/components/Form.js create mode 100644 src/js/components/Options.js create mode 100644 src/js/components/Popup.js create mode 100644 src/js/components/Select.js create mode 100644 src/js/components/Spinner.js create mode 100644 src/js/components/shared/Header.js create mode 100644 src/js/content.js create mode 100644 src/js/options.js create mode 100644 src/js/popup.js create mode 100644 src/js/remoteServices.js create mode 100644 src/js/utils/TimeInputParser.js create mode 100644 src/js/utils/browser.js create mode 100644 src/js/utils/index.js create mode 100644 src/js/utils/messageHandlers.js create mode 100644 src/js/utils/messaging.js create mode 100644 src/js/utils/notifier.js create mode 100644 src/js/utils/urlMatcher.js create mode 100644 src/manifest.json create mode 100644 src/options.html create mode 100644 src/popup.html create mode 100644 test/data.js create mode 100644 test/utils/TimeInputParser.test.js create mode 100644 test/utils/index.test.js create mode 100644 test/utils/urlMatcher.test.js create mode 100644 webpack.base.config.js create mode 100644 webpack.chrome.config.js create mode 100644 webpack.firefox.config.js create mode 100644 webpack/RemoveSourceMapPlugin.js create mode 100644 yarn.lock diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1a131b9 --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }], + ["@babel/plugin-proposal-optional-chaining"] + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c17f54b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,73 @@ +{ + "extends": ["eslint:recommended", "plugin:react/recommended"], + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true, + "jest/globals": true + }, + "globals": { + "chrome": false, + "browser": false + }, + "plugins": ["jest"], + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 7, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "experimentalDecorators": true, + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "strict": 0, + "semi": ["error", "never"], + "array-callback-return": "warn", + "getter-return": "warn", + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "warn", + "no-unreachable": "warn", + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "constructor-super": "warn", + "valid-typeof": "warn", + "jest/no-disabled-tests": "warn", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/prefer-to-have-length": "warn", + "jest/valid-expect": "error", + "react/prop-types": [ + "error", + { + "ignore": [ + "activityStore", + "commentStore", + "contactStore", + "dealStore", + "holidayStore", + "scheduleStore", + "presenceStore", + "employmentStore", + "match", + "permission", + "presenceStore", + "projectStore", + "purchaseBudgetStore", + "purchaseCategoryStore", + "purchasePaymentStore", + "userStore", + "vacationDailyHoursOverrideStore", + "purchaseStore", + "routing", + "scheduleStore", + "tagStore", + "taskTemplateStore", + "userStore" + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b38db2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..69045fb --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +mocoapp-browser-extension +========================= + +Documentation +------------- + +* 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 start:chrome` or `yarn start:firefox` (`yarn start` is an alias for `yarn start:chrome`) +* load extension into browser: + * Chrome: visit `chrome://extensions` and load unpacked extension from `build/chrome` + * Firefox: visit `about:debugging` and load temporary Add-on from `build/firefox` +* reload browser extension after change + +Release +------- + +* bump version in `package.json` +* run `yarn build` +* upload Chrome and Firefox extensions in `build/chrome` and `build/firefox` respectively diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4ba52ba --- /dev/null +++ b/jest.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 9539978..0000000 --- a/manifest.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "MOCO Zeiterfassung", - "short_name": "MOCO", - "description": "MOCO Zeiterfassung Plugin", - "version": "0.9.20", - "manifest_version": 2, - "description" : "MOCO Time Tracking Plugin", - "icons": { "16": "src/images/logo.png", - "48": "src/images/logo.png", - "128": "src/images/logo.png" }, - "options_ui": { - "page": "options.html", - "chrome_style": true - }, - "permissions": [ - "https://*.mocoapp.com/*", - "background", - "storage", - "tabs" - ], - "optional_permissions": [ - "*://*/" - ], - "background": { - "scripts": [ - "node_modules/jquery/dist/jquery.min.js", - "node_modules/select2/select2.js", - "background.min.js" - ] - }, - "browser_action": { - "default_icon": "src/images/logo.png", - "default_title": "MOCO Time Tracking", - "default_popup": "popup.html" - }, - "web_accessible_resources": [ - "src/images/*" - ], - "commands": { - "_execute_browser_action": { - "suggested_key": { - "default": "Ctrl+M", - "mac": "Command+M" - } - } - } -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e562130 --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "moco-browser-extensions", + "description": "Browser plugin for MOCO", + "version": "1.0.18", + "scripts": { + "start": "yarn start:chrome", + "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", + "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": "yarn run build:firefox && yarn run build:chrome", + "test": "node_modules/.bin/jest", + "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": { + "@bugsnag/js": "^5.2.0", + "@bugsnag/plugin-react": "^5.2.0", + "axios": "^0.18.0", + "classnames": "^2.2.6", + "date-fns": "^1.30.1", + "lodash": "^4.17.11", + "mobx": "^5.5.0", + "mobx-react": "^5.2.8", + "prop-types": "^15.6.2", + "query-string": "^6.2.0", + "react": "^16.8.0", + "react-dom": "^16.8.0", + "react-select": "^2.3.0", + "react-spring": "^8.0.7", + "url-pattern": "^1.0.3" + }, + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.2.2", + "@babel/plugin-proposal-decorators": "^7.2.2", + "@babel/plugin-proposal-optional-chaining": "^7.2.0", + "@babel/preset-env": "^7.2.2", + "@babel/preset-react": "^7.0.0", + "babel-eslint": "^10.0.1", + "babel-loader": "^8.0.4", + "babel-plugin-module-resolver": "^3.1.1", + "clean-webpack-plugin": "^1.0.1", + "copy-webpack-plugin": "^4.6.0", + "copyfiles": "^2.1.0", + "css-loader": "^2.1.0", + "eslint": "^5.7.0", + "eslint-plugin-jest": "^22.2.2", + "eslint-plugin-react": "^7.11.1", + "file-loader": "^3.0.1", + "html-webpack-plugin": "^3.2.0", + "jest": "^24.1.0", + "mini-css-extract-plugin": "^0.5.0", + "node-sass": "^4.11.0", + "prettier": "^1.16.4", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", + "webpack": "^4.15.0", + "webpack-bugsnag-plugins": "^1.3.0", + "webpack-cli": "^3.0.8", + "zip-webpack-plugin": "^3.0.0" + } +} diff --git a/src/background.html b/src/background.html new file mode 100644 index 0000000..0915a77 --- /dev/null +++ b/src/background.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/css/_button.scss b/src/css/_button.scss new file mode 100644 index 0000000..33ec648 --- /dev/null +++ b/src/css/_button.scss @@ -0,0 +1,42 @@ +button.moco-bx-btn { + display: block; + padding: 8px 12px; + margin: 0 auto; + font-weight: normal; + text-align: center; + white-space: nowrap; + color: white; + background-image: none; + background-color: #7dc332; + border-color: #7dc332; + border-radius: 0; + border-style: solid; + box-shadow: none; + font-size: 100%; + cursor: pointer; + + &:hover:not(:disabled) { + background-color: #639a28; + border-color: #639a28; + } + + &:disabled { + opacity: 0.65; + cursor: default; + } + + &.secondary { + color: black; + background-color: #fff; + border-color: #ccc; + + &:hover { + background-color: #f4f4f4; + border-color: #ccc; + } + } + + & + button { + margin-left: 0.5rem; + } +} diff --git a/src/css/_form.scss b/src/css/_form.scss new file mode 100644 index 0000000..233df13 --- /dev/null +++ b/src/css/_form.scss @@ -0,0 +1,91 @@ +@import "button"; + +input { + border-radius: 0; + height: 20px; +} + +.form-group { + width: 100%; + margin: 1rem 0; + + label { + display: block; + font-weight: bold; + margin-bottom: 0.25rem; + } + + input, textarea { + padding: 6px 12px; + background-color: white; + border-color: #cccccc; + width: 100%; + font-size: 100%; + border-style: solid; + border-width: 1px; + min-height: 20px; + } + + .text-muted { + color: #999; + } + + &.has-error { + input, textarea { + border-color: #FB3A2F; + } + } + + .form-error { + color: #FB3A2F; + } + + .input-group { + display: flex; + flex-flow: row nowrap; + + input { + flex: 1 1; + } + + .input-group-addon { + flex: 0 0 auto; + padding: 6px 12px; + font-weight: normal; + color: #555555; + text-align: center; + background-color: #eeeeee; + border: 1px solid #cccccc; + border-left: none; + line-height: 18px; + } + } +} + +.moco-bx-select + .moco-bx-select { + margin-top: 3px; +} + +input[name="hours"] { + width: 48px; + outline: 0 !important; + + &:focus { + border: 1px solid #38b5eb; + box-shadow: 0 0 0 1px #38b5eb; + } +} + +textarea[name="description"] { + font-family: inherit; + box-sizing: border-box; + width: 100%; + resize: none; + outline: 0 !important; + + &:focus { + border: 1px solid #38b5eb; + box-shadow: 0 0 0 1px #38b5eb; + } +} + diff --git a/src/css/_reset.scss b/src/css/_reset.scss new file mode 100644 index 0000000..7e126c4 --- /dev/null +++ b/src/css/_reset.scss @@ -0,0 +1,43 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/css/_spinner.scss b/src/css/_spinner.scss new file mode 100644 index 0000000..9270bb0 --- /dev/null +++ b/src/css/_spinner.scss @@ -0,0 +1,24 @@ +#moco-bx-root { + @keyframes moco-bx-spinner { + to { + transform: rotate(360deg); + } + } + + .moco-bx-spinner__container { + width: 100%; + margin-top: 120px; + + .moco-bx-spinner { + display: block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 2px solid #999; + border-right-color: transparent; + border-radius: 50%; + margin: 0 auto; + animation: moco-bx-spinner .75s linear infinite; + } + } +} diff --git a/src/css/_variables.scss b/src/css/_variables.scss new file mode 100644 index 0000000..d5b7a37 --- /dev/null +++ b/src/css/_variables.scss @@ -0,0 +1,5 @@ +$font-family: Arial, sans-serif; +$font-color: #191919; +$popup-width: 420px; +$popup-height: 463px; + diff --git a/src/css/content.scss b/src/css/content.scss new file mode 100644 index 0000000..de3b2cd --- /dev/null +++ b/src/css/content.scss @@ -0,0 +1,77 @@ +@import "variables"; +@import "button"; + +#moco-bx-root { + font-family: $font-family; + color: $font-color; + + .moco-bx-bubble { + box-sizing: content-box; + position: fixed; + bottom: 2rem; + height: 60px; + 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); + padding: 5px; + z-index: 9999; + + cursor: pointer; + + .moco-bx-bubble-inner { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: 100%; + + img.moco-bx-logo { + width: 30px; + height: 30px; + } + + .moco-bx-booked-hours { + display: inline-block; + font-size: 13px; + font-weight: 700; + line-height: 1; + color: black; + text-align: center; + padding: 5px 0 7px; + } + } + } +} + +#moco-bx-popup-root { + font-family: $font-family; + color: $font-color; + + iframe { + border: 0; + } + + h2 { + margin-bottom: 1rem; + } + + .moco-bx-popup { + position: fixed; /* Stay in place */ + z-index: 2000; /* Sit on top */ + padding-top: 100px; /* Location of the box */ + 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 */ + + .moco-bx-popup-content { + background-color: white; + margin: 0 auto; + } + } +} diff --git a/src/css/options.scss b/src/css/options.scss new file mode 100644 index 0000000..465fde3 --- /dev/null +++ b/src/css/options.scss @@ -0,0 +1,42 @@ +@import "variables"; +@import "reset"; +@import "form"; + +#moco-bx-root { + font-family: $font-family; + color: $font-color; + font-size: 14px; + line-height: 1.5; + + .moco-bx-options { + padding: 0rem 2rem 2rem; + + p { + margin: 0.5rem 0; + } + + h2 { + font-size: 1.5rem; + margin: 1rem 0 2rem; + } + + label { + font-weight: normal; + margin-bottom: 5px; + } + + input { + box-sizing: border-box; + padding: 6px 12px; + height: 32px; + } + + .text-success { + color: #7DC332; + } + + .text-danger { + color: #FB3A2F; + } + } +} diff --git a/src/css/popup.scss b/src/css/popup.scss new file mode 100644 index 0000000..312127b --- /dev/null +++ b/src/css/popup.scss @@ -0,0 +1,128 @@ +@import "form"; +@import "spinner"; +@import "variables"; + +html { + overflow: hidden; + + body { + font-size: 14px; + margin: 0px; + font-family: $font-family; + color: $font-color; + + #moco-bx-root { + min-width: 516px; + + .moco-bx-app-container { + width: 324px; + padding: 3rem 6rem; + + .moco-bx-logo__container { + display: flex; + justify-content: center; + margin-bottom: 3rem; + text-align: center; + + img.moco-bx-logo { + flex: 0 0 48px; + width: 48px; + height: 48px; + + } + + h1 { + line-height: 48px; + margin: 0; + } + } + + .moco-bx-calendar { + display: flex; + justify-content: space-between; + margin-bottom: 3rem; + + .moco-bx-calendar__day { + display: flex; + flex-flow: column nowrap; + align-items: center; + + flex: 0 0 42px; + width: 42px; + cursor: pointer; + + .moco-bx-calendar__day-of-week { + margin-bottom: 5px; + font-size: 12px; + color: hsl(0, 0%, 60%); + } + + .moco-bx-calendar__hours { + display: flex; + justify-content: center; + align-items: center; + width: 42px; + height: 42px; + flex: 0 0 42px; + color: white; + background-color: #eee; + + } + + &.moco-bx-calendar__day--filled { + .moco-bx-calendar__hours { + background-color: #7dc332; + } + } + + &.moco-bx-calendar__day--week-day-6, + &.moco-bx-calendar__day--week-day-0 { + .moco-bx-calendar__hours { + background-color: #bbb; + } + } + + &.moco-bx-calendar__day--active { + .moco-bx-calendar__hours { + background-color: #38b5eb; + } + } + } + } + } + + .moco-bx-error-container { + font-size: 18px; + line-height: 1.5; + width: 420px; + padding: 3rem; + text-align: center; + + h1 { + font-size: 35px; + font-weight: normal; + margin-top: 0; + line-height: 1.3; + } + + img { + width: auto; + max-width: 100%; + + &.moco-bx-logo { + width: 48px; + margin-bottom: 2rem; + } + } + + ol { + text-align: left; + } + + button { + margin-top: 1.5rem; + } + } + } + } +} diff --git a/src/images/error_general.png b/src/images/error_general.png new file mode 100644 index 0000000000000000000000000000000000000000..f243f137832fc77af962d28276c912a72fb25126 GIT binary patch literal 20549 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 E2L+q7j{pDw literal 0 HcmV?d00001 diff --git a/src/images/settings.png b/src/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..50bf38a87997b5ac3d10163e71016065f974414e GIT binary patch literal 29178 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 c?thSsqj*&XJX9d~ra-JaYWGyjmCb|x2i1Dd { + if (process.env.NODE_ENV === "production") { + return `https://${encodeURIComponent( + subdomain + )}.mocoapp.com/api/browser_extensions` + } else { + return `http://${encodeURIComponent( + subdomain + )}.mocoapp.localhost:3001/api/browser_extensions` + } +} + +export default class Client { + #client; + #apiKey; + + constructor({ subdomain, apiKey, version }) { + this.#apiKey = apiKey + this.#client = axios.create({ + responseType: "json", + baseURL: baseURL(subdomain), + headers: { + common: { + "x-api-key": apiKey, + "x-extension-version": version + } + } + }) + } + + login = service => + this.#client.post("session", { + api_key: this.#apiKey, + remote_service: service?.name, + remote_id: service?.id + }); + + projects = () => this.#client.get("projects"); + + schedules = (fromDate, toDate) => + this.#client.get("schedules", { + params: { date: `${formatDate(fromDate)}:${formatDate(toDate)}` } + }); + + activities = (fromDate, toDate) => + this.#client.get("activities", { + params: { date: `${formatDate(fromDate)}:${formatDate(toDate)}` } + }); + + bookedHours = service => { + if (!service) { + return Promise.resolve({ data: { hours: 0 } }) + } + return this.#client.get("activities/tags", { + params: { selection: [service.id], remote_service: service.name } + }) + }; + + createActivity = activity => this.#client.post("activities", { activity }); +} diff --git a/src/js/background.js b/src/js/background.js new file mode 100644 index 0000000..84f2df0 --- /dev/null +++ b/src/js/background.js @@ -0,0 +1,128 @@ +import ApiClient from "api/Client" +import { + isChrome, + getCurrentTab, + getSettings, + isBrowserTab +} from "utils/browser" +import { BackgroundMessenger } from "utils/messaging" +import { + tabUpdated, + settingsChanged, + togglePopup +} from "utils/messageHandlers" + +const messenger = new BackgroundMessenger() + +messenger.on("togglePopup", () => { + getCurrentTab().then(tab => { + if (tab && !isBrowserTab(tab)) { + messenger.postMessage(tab, { type: "requestService" }) + messenger.once("newService", ({ payload }) => { + togglePopup(tab, { messenger })(payload) + }) + } + }) +}) + +chrome.runtime.onMessage.addListener(action => { + if (action.type === "closePopup") { + getCurrentTab().then(tab => { + messenger.postMessage(tab, action) + }) + } + + if (action.type === "createActivity") { + const { activity, service } = action.payload + getCurrentTab().then(tab => { + getSettings().then(settings => { + const apiClient = new ApiClient(settings) + apiClient + .createActivity(activity) + .then(() => { + messenger.postMessage(tab, { type: "closePopup" }) + apiClient.bookedHours(service).then(({ data }) => { + messenger.postMessage(tab, { + type: "showBubble", + payload: { + bookedHours: parseFloat(data[0]?.hours) || 0, + service + } + }) + }) + }) + .catch(error => { + if (error.response?.status === 422) { + chrome.runtime.sendMessage({ + type: "setFormErrors", + payload: error.response.data + }) + } + }) + }) + }) + } + + if (action.type === "openOptions") { + let url + if (isChrome()) { + url = `chrome://extensions/?options=${chrome.runtime.id}` + } else { + url = browser.runtime.getURL("options.html") + } + return chrome.tabs.create({ url }) + } + + if (action.type === "openExtensions") { + if (isChrome()) { + chrome.tabs.create({ url: "chrome://extensions" }) + } + } +}) + +chrome.runtime.onInstalled.addListener(() => { + chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => { + if (areaName === "sync" && (apiKey || subdomain)) { + getSettings().then(settings => settingsChanged(settings, { messenger })) + } + }) +}) + +chrome.runtime.onStartup.addListener(() => { + chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => { + if (areaName === "sync" && (apiKey || subdomain)) { + getSettings().then(settings => settingsChanged(settings, { messenger })) + } + }) +}) + +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (!isBrowserTab(tab) && changeInfo.status === "complete") { + getSettings().then(settings => { + tabUpdated(tab, { settings, messenger }) + }) + } +}) + +chrome.tabs.onCreated.addListener(tab => { + if (!isBrowserTab(tab)) { + messenger.connectTab(tab) + } +}) + +chrome.tabs.onRemoved.addListener(messenger.disconnectTab) + +chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => { + if (areaName === "sync" && (apiKey || subdomain)) { + getSettings().then(settings => settingsChanged(settings, { messenger })) + } +}) + +chrome.browserAction.onClicked.addListener(tab => { + if (!isBrowserTab(tab)) { + messenger.postMessage(tab, { type: "requestService" }) + messenger.once("newService", ({ payload }) => { + togglePopup(tab, { messenger })(payload) + }) + } +}) diff --git a/src/js/components/App.js b/src/js/components/App.js new file mode 100644 index 0000000..defae02 --- /dev/null +++ b/src/js/components/App.js @@ -0,0 +1,212 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" +import Spinner from "components/Spinner" +import Form from "components/Form" +import Calendar from "components/Calendar" +import { observable, computed } from "mobx" +import { Observer, observer } from "mobx-react" +import { Spring, animated, config } from "react-spring/renderprops" +import { + ERROR_UNKNOWN, + ERROR_UNAUTHORIZED, + ERROR_UPGRADE_REQUIRED, + findProjectByValue, + findProjectByIdentifier, + findTask, + formatDate +} from "utils" +import InvalidConfigurationError from "components/Errors/InvalidConfigurationError" +import UpgradeRequiredError from "components/Errors/UpgradeRequiredError" +import UnknownError from "components/Errors/UnknownError" +import { parse } from "date-fns" +import Header from "./shared/Header" +import { head } from "lodash" +import TimeInputParser from "utils/TimeInputParser" + +@observer +class App extends Component { + static propTypes = { + loading: PropTypes.bool, + service: PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string, + name: PropTypes.string, + description: PropTypes.string, + projectId: PropTypes.string, + taskId: PropTypes.string + }), + activities: PropTypes.array, + schedules: PropTypes.array, + projects: PropTypes.array, + lastProjectId: PropTypes.number, + lastTaskId: PropTypes.number, + roundTimeEntries: PropTypes.bool, + fromDate: PropTypes.string, + toDate: PropTypes.string, + errorType: PropTypes.string, + errorMessage: PropTypes.string + }; + + static defaultProps = { + activities: [], + schedules: [], + projects: [], + roundTimeEntries: false + }; + + @observable changeset = {}; + @observable formErrors = {}; + + @computed get changesetWithDefaults() { + const { service, projects, lastProjectId, lastTaskId } = this.props + + const project = + findProjectByIdentifier(service?.projectId)(projects) || + findProjectByValue(Number(lastProjectId))(projects) || + head(projects) + + const task = + findTask(service?.taskId || lastTaskId)(project) || head(project?.tasks) + + const defaults = { + remote_service: service?.name, + remote_id: service?.id, + remote_url: service?.url, + date: formatDate(new Date()), + assignment_id: project?.value, + task_id: task?.value, + billable: task?.billable, + hours: "", + seconds: + this.changeset.hours && + new TimeInputParser(this.changeset.hours).parseSeconds(), + description: service?.description + } + + return { + ...defaults, + ...this.changeset + } + } + + componentDidMount() { + window.addEventListener("keydown", this.handleKeyDown) + chrome.runtime.onMessage.addListener(this.handleSetFormErrors) + } + + componentWillUnmount() { + window.removeEventListener("keydown", this.handleKeyDown) + chrome.runtime.onMessage.removeListener(this.handleSetFormErrors) + } + + handleChange = event => { + const { projects } = this.props + const { + target: { name, value } + } = event + + this.changeset[name] = value + + if (name === "assignment_id") { + const project = findProjectByValue(value)(projects) + this.changeset.task_id = head(project?.tasks).value || null + } + }; + + handleSelectDate = date => { + this.changeset.date = formatDate(date) + }; + + handleSubmit = event => { + event.preventDefault() + const { service } = this.props + + chrome.runtime.sendMessage({ + type: "createActivity", + payload: { + activity: this.changesetWithDefaults, + service + } + }) + }; + + handleKeyDown = event => { + if (event.keyCode === 27) { + event.stopPropagation() + chrome.runtime.sendMessage({ type: "closePopup" }) + } + }; + + handleSetFormErrors = ({ type, payload }) => { + if (type === "setFormErrors") { + this.formErrors = payload + } + }; + + render() { + const { + loading, + projects, + activities, + schedules, + fromDate, + toDate, + errorType, + errorMessage + } = this.props + + if (loading) { + return + } + + if (errorType === ERROR_UNAUTHORIZED) { + return + } + + if (errorType === ERROR_UPGRADE_REQUIRED) { + return + } + + if (errorType === ERROR_UNKNOWN) { + return + } + + return ( + + {props => ( + +
+ + {() => ( + <> + +
+ + )} + + + )} + + ) + } +} + +export default App diff --git a/src/js/components/Bubble.js b/src/js/components/Bubble.js new file mode 100644 index 0000000..22b7f29 --- /dev/null +++ b/src/js/components/Bubble.js @@ -0,0 +1,23 @@ +import React from "react" +import PropTypes from "prop-types" +import logoUrl from "images/logo.png" + +const Bubble = ({ bookedHours, onClick }) => ( +
+ + {bookedHours > 0 ? ( + {bookedHours.toFixed(2)} + ) : null} +
+) + +Bubble.propTypes = { + bookedHours: PropTypes.number, + onClick: PropTypes.func.isRequired +} + +Bubble.defaultProps = { + bookedHours: 0 +} + +export default Bubble diff --git a/src/js/components/Calendar/Day.js b/src/js/components/Calendar/Day.js new file mode 100644 index 0000000..1e9afa7 --- /dev/null +++ b/src/js/components/Calendar/Day.js @@ -0,0 +1,40 @@ +import React, { useCallback } from "react" +import PropTypes from "prop-types" +import Hours from "./Hours" +import { format, getDay } from "date-fns" +import deLocale from "date-fns/locale/de" +import cn from "classnames" + +const Day = ({ date, hours, absence, active, onClick }) => { + const handleClick = useCallback(() => onClick(date), [date]) + + return ( +
0, + "moco-bx-calendar__day--absence": absence + } + )} + onClick={handleClick} + > + + {format(date, "dd", { locale: deLocale })} + + +
+ ) +} + +Day.propTypes = { + date: PropTypes.instanceOf(Date).isRequired, + hours: PropTypes.number.isRequired, + absence: PropTypes.object, + active: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired +} + +export default Day diff --git a/src/js/components/Calendar/Hours.js b/src/js/components/Calendar/Hours.js new file mode 100644 index 0000000..04ca61e --- /dev/null +++ b/src/js/components/Calendar/Hours.js @@ -0,0 +1,43 @@ +import React from "react" +import PropTypes from "prop-types" + +const Hours = ({ hours, absence, active }) => { + let style + let content = null + + if (hours > 0) { + content = hours.toFixed(1) + } else if (absence) { + if (!active) { + style = { backgroundColor: absence.assignment_color } + } + + content = + absence.assignment_code === "1" + ? "/" + : absence.assignment_code === "2" + ? "★" + : absence.assignment_code === "3" + ? "K" + : absence.assignment_code === "4" + ? "✈" + : null + } + + return ( + + {content} + + ) +} + +Hours.propTypes = { + hours: PropTypes.number.isRequired, + absence: PropTypes.shape({ + assignment_code: PropTypes.string, + assignment_color: PropTypes.string + }), + active: PropTypes.bool.isRequired +} + +export default Hours diff --git a/src/js/components/Calendar/index.js b/src/js/components/Calendar/index.js new file mode 100644 index 0000000..a29613f --- /dev/null +++ b/src/js/components/Calendar/index.js @@ -0,0 +1,60 @@ +import React from "react" +import PropTypes from "prop-types" +import Day from "./Day" +import { formatDate } from "utils" +import { eachDay } from "date-fns" +import { pathEq } from "lodash/fp" + +const findAbsence = (date, schedules) => + schedules.find(pathEq("date", formatDate(date))) + +const hoursAtDate = (date, activities) => + activities + .filter(pathEq("date", formatDate(date))) + .reduce((acc, activity) => acc + activity.hours, 0) + +const Calendar = ({ + fromDate, + toDate, + selectedDate, + activities, + schedules, + onChange +}) => ( +
+ {eachDay(fromDate, toDate).map(date => ( + + ))} +
+) + +Calendar.propTypes = { + fromDate: PropTypes.instanceOf(Date).isRequired, + toDate: PropTypes.instanceOf(Date).isRequired, + selectedDate: PropTypes.instanceOf(Date).isRequired, + activities: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + date: PropTypes.string.isRequired, + hours: PropTypes.number.isRequired, + timer_started_at: PropTypes.string + }).isRequired + ), + schedules: PropTypes.arrayOf( + PropTypes.shape({ + date: PropTypes.string, + assignment_code: PropTypes.string, + assignment_color: PropTypes.string + }) + ).isRequired, + onChange: PropTypes.func.isRequired +} + +export default Calendar diff --git a/src/js/components/Errors/InvalidConfigurationError.js b/src/js/components/Errors/InvalidConfigurationError.js new file mode 100644 index 0000000..985a858 --- /dev/null +++ b/src/js/components/Errors/InvalidConfigurationError.js @@ -0,0 +1,28 @@ +import React from "react" +import settingsUrl from "images/settings.png" + +const InvalidConfigurationError = () => ( +
+

Bitte Einstellungen aktualisieren

+
    +
  1. Internetadresse eintragen
  2. +
  3. Persönlichen API-Schlüssel eintragen
  4. +
+ +
+
+ Browser extension configuration settings chrome.runtime.sendMessage({ type: "openOptions" })} + /> +
+) + +export default InvalidConfigurationError diff --git a/src/js/components/Errors/UnknownError.js b/src/js/components/Errors/UnknownError.js new file mode 100644 index 0000000..c0eba26 --- /dev/null +++ b/src/js/components/Errors/UnknownError.js @@ -0,0 +1,21 @@ +import React from "react" +import PropTypes from "prop-types" +import logo from "images/logo.png" + +const UnknownError = ({ message = "Unbekannter Fehler" }) => ( +
+ MOCO logo +

Ups, es ist ein Fehler passiert!

+

Bitte überprüfe deine Internetverbindung.

+

Wir wurden per Email benachrichtigt und untersuchen den Vorfall.

+
+

Fehlermeldung:

+
{message}
+
+) + +UnknownError.propTypes = { + message: PropTypes.string +} + +export default UnknownError diff --git a/src/js/components/Errors/UpgradeRequiredError.js b/src/js/components/Errors/UpgradeRequiredError.js new file mode 100644 index 0000000..a5e416f --- /dev/null +++ b/src/js/components/Errors/UpgradeRequiredError.js @@ -0,0 +1,35 @@ +import React from "react" +import { isChrome } from "utils/browser" +import logo from "images/logo.png" +import firefoxAddons from "images/firefox_addons.png" + +const UpgradeRequiredError = () => ( +
+ MOCO logo +

Upgrade erforderlich

+

+ Die installierte MOCO Browser-Erweiterung ist veraltet — bitte + aktualisieren. +

+ {isChrome() ? ( + + ) : ( + <> +
+

Unter folgender URL:

+ about:addons + + )} +
+) + +export default UpgradeRequiredError diff --git a/src/js/components/Form.js b/src/js/components/Form.js new file mode 100644 index 0000000..f429c51 --- /dev/null +++ b/src/js/components/Form.js @@ -0,0 +1,113 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" +import Select from "components/Select" +import cn from "classnames" + +class Form extends Component { + static propTypes = { + changeset: PropTypes.shape({ + project: PropTypes.object, + task: PropTypes.object, + hours: PropTypes.string + }).isRequired, + errors: PropTypes.object, + projects: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired + }; + + static defaultProps = { + inline: true + }; + + isValid = () => { + const { changeset } = this.props + return ["assignment_id", "task_id", "hours", "description"] + .map(prop => changeset[prop]) + .every(Boolean) + }; + + handleTextareaKeyDown = event => { + const { onSubmit } = this.props + + if (event.key === "Enter") { + event.preventDefault() + this.isValid() && onSubmit(event) + } + }; + + render() { + const { projects, changeset, errors, onChange, onSubmit } = this.props + const project = Select.findOptionByValue(projects, changeset.assignment_id) + + return ( + +
+ "Zuerst Projekt wählen"} + /> + {errors.assignment_id ? ( +
{errors.assignment_id.join("; ")}
+ ) : null} + {errors.task_id ? ( +
{errors.task_id.join("; ")}
+ ) : null} +
+
+ + {errors.hours ? ( +
{errors.hours.join("; ")}
+ ) : null} +
+
+