Refactor App component to load projects and create activity
This commit is contained in:
@@ -5,14 +5,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moco-bx-spinner {
|
.moco-bx-spinner__container {
|
||||||
display: inline-block;
|
width: 100%;
|
||||||
width: 2rem;
|
height: 100%;
|
||||||
height: 2rem;
|
display: flex;
|
||||||
vertical-align: text-bottom;
|
justify-content: center;
|
||||||
border: 2px solid #999;
|
align-items: center;
|
||||||
border-right-color: transparent;
|
|
||||||
border-radius: 50%;
|
.moco-bx-spinner {
|
||||||
animation: moco-bx-spinner .75s linear infinite;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
@import "form";
|
@import "form";
|
||||||
@import "spinner";
|
@import "spinner";
|
||||||
|
|
||||||
#moco-bx-root {
|
html {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
#moco-bx-root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
|
|||||||
import ApiClient from "api/Client"
|
import ApiClient from "api/Client"
|
||||||
import Form from "components/Form"
|
import Form from "components/Form"
|
||||||
import Spinner from "components/Spinner"
|
import Spinner from "components/Spinner"
|
||||||
import { observable, computed, reaction } from "mobx"
|
import { observable, computed, toJS } from "mobx"
|
||||||
import { observer, disposeOnUnmount } from "mobx-react"
|
import { observer, disposeOnUnmount } from "mobx-react"
|
||||||
import {
|
import {
|
||||||
findLastProject,
|
findLastProject,
|
||||||
@@ -25,24 +25,30 @@ class App extends Component {
|
|||||||
projectId: PropTypes.string,
|
projectId: PropTypes.string,
|
||||||
taskId: PropTypes.string
|
taskId: PropTypes.string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
projects: PropTypes.arrayOf(PropTypes.object).isRequired,
|
settings: PropTypes.shape({
|
||||||
lastProjectId: PropTypes.number,
|
subdomain: PropTypes.string,
|
||||||
lastTaskId: PropTypes.number,
|
apiKey: PropTypes.string,
|
||||||
|
version: PropTypes.string
|
||||||
|
}),
|
||||||
browser: PropTypes.object.isRequired
|
browser: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@observable projects = []
|
||||||
|
@observable lastProjectId
|
||||||
|
@observable lastTaskId
|
||||||
@observable changeset = {};
|
@observable changeset = {};
|
||||||
@observable formErrors = {};
|
@observable formErrors = {};
|
||||||
|
@observable isLoading = true
|
||||||
|
|
||||||
@computed get changesetWithDefaults() {
|
@computed get changesetWithDefaults() {
|
||||||
const { service, projects, lastProjectId, lastTaskId } = this.props
|
const { service } = this.props
|
||||||
|
|
||||||
const project =
|
const project =
|
||||||
findLastProject(service.projectId || lastProjectId)(projects) ||
|
findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
|
||||||
head(projects)
|
head(this.projects)
|
||||||
|
|
||||||
const task =
|
const task =
|
||||||
findLastTask(service.taskId || lastTaskId)(project) ||
|
findLastTask(service.taskId || this.lastTaskId)(project) ||
|
||||||
head(project?.tasks)
|
head(project?.tasks)
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
@@ -64,7 +70,15 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#apiClient
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.initializeApiClient(props.settings)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.fetchProjects()
|
||||||
window.addEventListener("keydown", this.handleKeyDown)
|
window.addEventListener("keydown", this.handleKeyDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +86,51 @@ class App extends Component {
|
|||||||
window.removeEventListener("keydown", this.handleKeyDown)
|
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 => {
|
handleKeyDown = event => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
@@ -93,7 +152,7 @@ class App extends Component {
|
|||||||
|
|
||||||
handleSubmit = event => {
|
handleSubmit = event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.sendMessage({ type: 'submitForm', payload: this.changesetWithDefaults })
|
this.createActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancel = () => {
|
handleCancel = () => {
|
||||||
@@ -107,12 +166,16 @@ class App extends Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { service, projects } = this.props;
|
if (this.isLoading) {
|
||||||
|
return <Spinner />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { service } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
changeset={this.changesetWithDefaults}
|
changeset={this.changesetWithDefaults}
|
||||||
projects={projects}
|
projects={this.projects}
|
||||||
errors={this.formErrors}
|
errors={this.formErrors}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onSubmit={this.handleSubmit}
|
onSubmit={this.handleSubmit}
|
||||||
|
|||||||
@@ -40,12 +40,7 @@ class Bubble extends Component {
|
|||||||
|
|
||||||
@observable isLoading = false;
|
@observable isLoading = false;
|
||||||
@observable isOpen = false;
|
@observable isOpen = false;
|
||||||
@observable projects = [];
|
|
||||||
@observable lastProjectId;
|
|
||||||
@observable lastTaskId;
|
|
||||||
@observable bookedHours = 0;
|
@observable bookedHours = 0;
|
||||||
@observable changeset = {};
|
|
||||||
@observable formErrors = {};
|
|
||||||
@observable unauthorizedError = false;
|
@observable unauthorizedError = false;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -57,9 +52,9 @@ class Bubble extends Component {
|
|||||||
disposeOnUnmount(
|
disposeOnUnmount(
|
||||||
this,
|
this,
|
||||||
reaction(() => this.props.settings, settings => {
|
reaction(() => this.props.settings, settings => {
|
||||||
|
this.close()
|
||||||
this.initializeApiClient(settings)
|
this.initializeApiClient(settings)
|
||||||
this.fetchBookedHours()
|
this.fetchBookedHours()
|
||||||
this.close()
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,12 +73,15 @@ class Bubble extends Component {
|
|||||||
window.removeEventListener("keydown", this.handleKeyDown)
|
window.removeEventListener("keydown", this.handleKeyDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeApiClient = settings => {
|
||||||
|
this.#apiClient = new ApiClient(settings)
|
||||||
|
}
|
||||||
|
|
||||||
open = event => {
|
open = event => {
|
||||||
if (event && event.target && event.target.classList.contains('moco-bx-popup')) {
|
if (event && event.target && event.target.classList.contains('moco-bx-popup')) {
|
||||||
return this.close()
|
return this.close()
|
||||||
}
|
}
|
||||||
this.isOpen = true
|
this.isOpen = true
|
||||||
this.fetchProjects().then(() => (this.isOpen = true))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
close = _event => {
|
close = _event => {
|
||||||
@@ -92,8 +90,9 @@ class Bubble extends Component {
|
|||||||
|
|
||||||
receiveMessage = ({ type, payload }) => {
|
receiveMessage = ({ type, payload }) => {
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'submitForm': {
|
case 'activityCreated': {
|
||||||
return this.createActivity(payload).then(() => this.close())
|
this.bookedHours += payload.hours
|
||||||
|
return this.close()
|
||||||
}
|
}
|
||||||
case 'closeForm': {
|
case 'closeForm': {
|
||||||
return this.close()
|
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 = () => {
|
fetchBookedHours = () => {
|
||||||
const { service } = this.props
|
const { service } = this.props
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
@@ -148,26 +118,6 @@ class Bubble extends Component {
|
|||||||
.finally(() => (this.isLoading = false))
|
.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 => {
|
handleKeyDown = event => {
|
||||||
if (event.key === 'm' && (event.metaKey || event.ctrlKey)) {
|
if (event.key === 'm' && (event.metaKey || event.ctrlKey)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -187,7 +137,7 @@ class Bubble extends Component {
|
|||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { service, browser } = this.props;
|
const { service, settings, browser } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="moco-bx-bubble" onClick={this.open}>
|
<div className="moco-bx-bubble" onClick={this.open}>
|
||||||
@@ -199,9 +149,7 @@ class Bubble extends Component {
|
|||||||
{this.isOpen && (
|
{this.isOpen && (
|
||||||
<Popup
|
<Popup
|
||||||
service={service}
|
service={service}
|
||||||
projects={this.projects}
|
settings={settings}
|
||||||
lastProjectId={this.lastProjectId}
|
|
||||||
lastTaskId={this.lastTaskId}
|
|
||||||
browser={browser}
|
browser={browser}
|
||||||
unauthorizedError={this.unauthorizedError}
|
unauthorizedError={this.unauthorizedError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import queryString from 'query-string'
|
|||||||
import { serializeProps } from 'utils'
|
import { serializeProps } from 'utils'
|
||||||
|
|
||||||
const Popup = props => {
|
const Popup = props => {
|
||||||
const serializedProps = serializeProps(
|
const serializedProps = serializeProps(['service', 'settings'])(props)
|
||||||
['service', 'projects', 'lastProjectId', 'lastTaskId']
|
|
||||||
)(props)
|
|
||||||
|
|
||||||
const styles = useMemo(() => ({
|
const styles = useMemo(() => ({
|
||||||
width: '536px',
|
width: '536px',
|
||||||
@@ -31,9 +29,6 @@ const Popup = props => {
|
|||||||
|
|
||||||
Popup.propTypes = {
|
Popup.propTypes = {
|
||||||
service: PropTypes.object.isRequired,
|
service: PropTypes.object.isRequired,
|
||||||
projects: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
lastProjectId: PropTypes.number,
|
|
||||||
lastTaskId: PropTypes.number,
|
|
||||||
browser: PropTypes.object.isRequired,
|
browser: PropTypes.object.isRequired,
|
||||||
unauthorizedError: PropTypes.bool.isRequired
|
unauthorizedError: PropTypes.bool.isRequired
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const Spinner = () => (
|
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
|
export default Spinner
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import queryString from 'query-string'
|
|||||||
import { parseProps } from 'utils'
|
import { parseProps } from 'utils'
|
||||||
import '../css/popup.scss'
|
import '../css/popup.scss'
|
||||||
|
|
||||||
const parsedProps = parseProps(
|
const parsedProps = parseProps(['service', 'settings'])(
|
||||||
['service', 'projects', 'lastProjectId', 'lastTaskId']
|
queryString.parse(location.search)
|
||||||
)(queryString.parse(location.search))
|
)
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App
|
<App
|
||||||
|
|||||||
Reference in New Issue
Block a user