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
This commit is contained in:
Manuel Bouza
2019-03-22 15:56:24 +01:00
parent cbf79b960c
commit 28a9a86e27
58 changed files with 11017 additions and 47 deletions

212
src/js/components/App.js Normal file
View File

@@ -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 <Spinner />
}
if (errorType === ERROR_UNAUTHORIZED) {
return <InvalidConfigurationError />
}
if (errorType === ERROR_UPGRADE_REQUIRED) {
return <UpgradeRequiredError />
}
if (errorType === ERROR_UNKNOWN) {
return <UnknownError message={errorMessage} />
}
return (
<Spring
native
from={{ opacity: 0 }}
to={{ opacity: 1 }}
config={config.stiff}
>
{props => (
<animated.div className="moco-bx-app-container" style={props}>
<Header />
<Observer>
{() => (
<>
<Calendar
fromDate={parse(fromDate)}
toDate={parse(toDate)}
activities={activities}
schedules={schedules}
selectedDate={new Date(this.changesetWithDefaults.date)}
onChange={this.handleSelectDate}
/>
<Form
changeset={this.changesetWithDefaults}
projects={projects}
errors={this.formErrors}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
/>
</>
)}
</Observer>
</animated.div>
)}
</Spring>
)
}
}
export default App