Refactor App component to load projects and create activity

This commit is contained in:
Manuel Bouza
2019-02-21 08:05:56 +01:00
parent 8811f6d382
commit b65fd3a5f0
7 changed files with 119 additions and 94 deletions

View File

@@ -5,14 +5,22 @@
}
}
.moco-bx-spinner {
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: text-bottom;
border: 2px solid #999;
border-right-color: transparent;
border-radius: 50%;
animation: moco-bx-spinner .75s linear infinite;
.moco-bx-spinner__container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.moco-bx-spinner {
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: text-bottom;
border: 2px solid #999;
border-right-color: transparent;
border-radius: 50%;
animation: moco-bx-spinner .75s linear infinite;
}
}
}

View File

@@ -1,5 +1,14 @@
@import "form";
@import "spinner";
#moco-bx-root {
html {
height: 100%;
body {
height: 100%;
#moco-bx-root {
height: 100%;
}
}
}

View File

@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
import ApiClient from "api/Client"
import Form from "components/Form"
import Spinner from "components/Spinner"
import { observable, computed, reaction } from "mobx"
import { observable, computed, toJS } from "mobx"
import { observer, disposeOnUnmount } from "mobx-react"
import {
findLastProject,
@@ -25,24 +25,30 @@ class App extends Component {
projectId: PropTypes.string,
taskId: PropTypes.string
}).isRequired,
projects: PropTypes.arrayOf(PropTypes.object).isRequired,
lastProjectId: PropTypes.number,
lastTaskId: PropTypes.number,
settings: PropTypes.shape({
subdomain: PropTypes.string,
apiKey: PropTypes.string,
version: PropTypes.string
}),
browser: PropTypes.object.isRequired
};
@observable projects = []
@observable lastProjectId
@observable lastTaskId
@observable changeset = {};
@observable formErrors = {};
@observable isLoading = true
@computed get changesetWithDefaults() {
const { service, projects, lastProjectId, lastTaskId } = this.props
const { service } = this.props
const project =
findLastProject(service.projectId || lastProjectId)(projects) ||
head(projects)
findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
head(this.projects)
const task =
findLastTask(service.taskId || lastTaskId)(project) ||
findLastTask(service.taskId || this.lastTaskId)(project) ||
head(project?.tasks)
const defaults = {
@@ -64,14 +70,67 @@ class App extends Component {
}
}
#apiClient
constructor(props) {
super(props)
this.initializeApiClient(props.settings)
}
componentDidMount() {
this.fetchProjects()
window.addEventListener("keydown", this.handleKeyDown)
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleKeyDown)
}
initializeApiClient = settings => {
this.#apiClient = new ApiClient(settings)
}
fetchProjects = () => {
this.isLoading = true
return this.#apiClient
.projects()
.then(({ data }) => {
this.projects = groupedProjectOptions(data.projects)
this.lastProjectId = data.last_project_id
this.lastTaskId = data.lastTaskId
})
.catch(error => {
console.log(error)
})
.finally(() => {
this.isLoading = false
})
};
createActivity = () => {
this.isLoading = true
this.#apiClient
.createActivity(this.changesetWithDefaults)
.then(({ data }) => {
this.changeset = {}
this.formErrors = {}
this.sendMessage(
{ type: 'activityCreated', payload: { hours: data.hours} }
)
})
.catch(error => {
if (error.response?.status === 422) {
this.formErrors = error.response.data
}
if (error.response?.status === 401) {
this.unauthorizedError = true
}
})
.finally(() => this.isLoading = false)
}
handleKeyDown = event => {
event.stopPropagation()
if (event.keyCode === 27) {
@@ -93,7 +152,7 @@ class App extends Component {
handleSubmit = event => {
event.preventDefault()
this.sendMessage({ type: 'submitForm', payload: this.changesetWithDefaults })
this.createActivity()
}
handleCancel = () => {
@@ -107,12 +166,16 @@ class App extends Component {
)
render() {
const { service, projects } = this.props;
if (this.isLoading) {
return <Spinner />
}
const { service } = this.props;
return (
<Form
changeset={this.changesetWithDefaults}
projects={projects}
projects={this.projects}
errors={this.formErrors}
onChange={this.handleChange}
onSubmit={this.handleSubmit}

View File

@@ -40,12 +40,7 @@ class Bubble extends Component {
@observable isLoading = false;
@observable isOpen = false;
@observable projects = [];
@observable lastProjectId;
@observable lastTaskId;
@observable bookedHours = 0;
@observable changeset = {};
@observable formErrors = {};
@observable unauthorizedError = false;
constructor(props) {
@@ -57,9 +52,9 @@ class Bubble extends Component {
disposeOnUnmount(
this,
reaction(() => this.props.settings, settings => {
this.close()
this.initializeApiClient(settings)
this.fetchBookedHours()
this.close()
})
)
@@ -78,12 +73,15 @@ class Bubble extends Component {
window.removeEventListener("keydown", this.handleKeyDown)
}
initializeApiClient = settings => {
this.#apiClient = new ApiClient(settings)
}
open = event => {
if (event && event.target && event.target.classList.contains('moco-bx-popup')) {
return this.close()
}
this.isOpen = true
this.fetchProjects().then(() => (this.isOpen = true))
};
close = _event => {
@@ -92,8 +90,9 @@ class Bubble extends Component {
receiveMessage = ({ type, payload }) => {
switch(type) {
case 'submitForm': {
return this.createActivity(payload).then(() => this.close())
case 'activityCreated': {
this.bookedHours += payload.hours
return this.close()
}
case 'closeForm': {
return this.close()
@@ -101,35 +100,6 @@ class Bubble extends Component {
}
}
initializeApiClient = settings => {
this.#apiClient = new ApiClient(settings)
}
fetchProjects = () => {
if (this.projects.length > 0) {
return Promise.resolve();
}
this.isLoading = true
return this.#apiClient
.projects()
.then(({ data }) => {
this.unauthorizedError = false
this.projects = groupedProjectOptions(data.projects)
this.lastProjectId = data.last_project_id
this.lastTaskId = data.lastTaskId
})
.catch(error => {
if (error.response?.status === 401) {
this.unauthorizedError = true
}
})
.finally(() => {
this.isLoading = false
})
};
fetchBookedHours = () => {
const { service } = this.props
this.isLoading = true
@@ -148,26 +118,6 @@ class Bubble extends Component {
.finally(() => (this.isLoading = false))
};
createActivity = payload =>
this.#apiClient
.createActivity(payload)
.then(({ data }) => {
this.bookedHours += data.hours
this.changeset = {}
this.formErrors = {}
this.unauthorizedError = false
})
.catch(this.handleSubmitError)
handleSubmitError = error => {
if (error.response?.status === 422) {
this.formErrors = error.response.data
}
if (error.response?.status === 401) {
this.unauthorizedError = true
}
};
handleKeyDown = event => {
if (event.key === 'm' && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
@@ -187,7 +137,7 @@ class Bubble extends Component {
return <Spinner />
}
const { service, browser } = this.props;
const { service, settings, browser } = this.props;
return (
<div className="moco-bx-bubble" onClick={this.open}>
@@ -199,9 +149,7 @@ class Bubble extends Component {
{this.isOpen && (
<Popup
service={service}
projects={this.projects}
lastProjectId={this.lastProjectId}
lastTaskId={this.lastTaskId}
settings={settings}
browser={browser}
unauthorizedError={this.unauthorizedError}
/>

View File

@@ -5,9 +5,7 @@ import queryString from 'query-string'
import { serializeProps } from 'utils'
const Popup = props => {
const serializedProps = serializeProps(
['service', 'projects', 'lastProjectId', 'lastTaskId']
)(props)
const serializedProps = serializeProps(['service', 'settings'])(props)
const styles = useMemo(() => ({
width: '536px',
@@ -31,9 +29,6 @@ const Popup = props => {
Popup.propTypes = {
service: PropTypes.object.isRequired,
projects: PropTypes.arrayOf(PropTypes.object).isRequired,
lastProjectId: PropTypes.number,
lastTaskId: PropTypes.number,
browser: PropTypes.object.isRequired,
unauthorizedError: PropTypes.bool.isRequired
}

View File

@@ -1,7 +1,9 @@
import React from 'react'
const Spinner = () => (
<div className="moco-bx-spinner" role="status" />
<div className='moco-bx-spinner__container'>
<div className='moco-bx-spinner' role='status' />
</div>
)
export default Spinner

View File

@@ -5,9 +5,9 @@ import queryString from 'query-string'
import { parseProps } from 'utils'
import '../css/popup.scss'
const parsedProps = parseProps(
['service', 'projects', 'lastProjectId', 'lastTaskId']
)(queryString.parse(location.search))
const parsedProps = parseProps(['service', 'settings'])(
queryString.parse(location.search)
)
ReactDOM.render(
<App