Load projects and initialize form with last project and task

This commit is contained in:
Manuel Bouza
2019-02-06 18:55:10 +01:00
parent 49aa36bf54
commit 7ad7cab5c0
16 changed files with 783 additions and 125 deletions

View File

@@ -1,63 +1,26 @@
import axios from "axios"
class Client {
constructor() {
this.client = axios.create({
responseType: "json"
export default class Client {
#client
#apiKey
constructor({ subdomain, apiKey, clientVersion }) {
this.#apiKey = apiKey
this.#client = axios.create({
responseType: "json",
baseURL: `https://${encodeURIComponent(
subdomain
)}.mocoapp.com/api/browser_extensions`,
headers: {
common: {
"x-api-key": apiKey,
"x-client-version": clientVersion
}
}
})
}
registerStorage(storage) {
storage.sync.get(["subdomain", "apiKey"], store => {
this.setSubdomain(store.subdomain)
this.setCredentials(store.apiKey)
})
login = () => this.#client.post("session", { api_key: this.#apiKey })
storage.onChanged.addListener(({ subdomain, apiKey }) => {
subdomain && this.setSubdomain(subdomain.newValue)
apiKey && this.setCredentials(apiKey.newValue)
})
}
setSubdomain(subdomain) {
this.client.defaults.baseURL = `https://${encodeURIComponent(
subdomain
)}.mocoapp.com/api/v1`
}
setCredentials(apiKey) {
this.client.defaults.headers.common[
"Authorization"
] = `Token token=${encodeURIComponent(apiKey)}`
}
setClientVersion(version) {
this.client.defaults.headers.common["x-client-version"] = version
}
get defaults() {
return this.client.defaults
}
get(url, config = {}) {
return this.client.get(url, config)
}
post(url, data) {
return this.client.post(url, data)
}
put(url, data) {
return this.client.put(url, data)
}
patch(url, data) {
return this.client.patch(url, data)
}
delete(url) {
return this.client.delete(url)
}
projects = () => this.#client.get("projects")
}
export default new Client()

View File

@@ -1,5 +0,0 @@
import client from './Client'
export function getProjects(subdomain, apiKey) {
return client.post(`https://${subdomain}.mocoapp.com/api/browser_extensions/session`, { api_key: apiKey })
}

View File

@@ -1,5 +0,0 @@
import client from './Client'
export function login(subdomain, apiKey) {
return client.post(`https://${subdomain}.mocoapp.com/api/browser_extensions/session`, { api_key: apiKey })
}

View File

@@ -1,11 +1,8 @@
import DomainCheck from "./services/DomainCheck"
import apiClient from "api/client"
import config from "./config"
apiClient.registerStorage(chrome.storage)
apiClient.setClientVersion(chrome.runtime.getManifest().version)
const domainCheck = new DomainCheck(config)
const { version } = chrome.runtime.getManifest()
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// run only after the page is fully loaded
@@ -16,9 +13,16 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
const service = domainCheck.match(tab.url)
if (service) {
chrome.tabs.sendMessage(tabId, { type: "mountBubble", service }, () => {
console.log("bubble mounted")
})
chrome.storage.sync.get(
["subdomain", "apiKey"],
({ subdomain, apiKey }) => {
const settings = { subdomain, apiKey, version }
const payload = { service, settings }
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
console.log("bubble mounted")
})
}
)
} else {
chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }, () => {
console.log("bubble unmounted")

View File

@@ -1,42 +1,128 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ApiClient from "api/Client"
import Modal, { Content } from "components/Modal"
import Form from "components/Form"
import { observable } from "mobx"
import { observer } from "mobx-react"
import logoUrl from "../../images/logo.png"
import logoUrl from "images/logo.png"
import get from "lodash/get"
import { findLastProject, findLastTask, groupedProjectOptions } from "utils"
@observer
class Bubble extends Component {
@observable open = false
static propTypes = {
settings: PropTypes.shape({
subdomain: PropTypes.string,
apiKey: PropTypes.string,
version: PropTypes.string
})
};
onOpen = _event => {
this.open = true
}
onClose = _event => {
this.open = false
}
@observable isLoading = true;
@observable isOpen = false;
@observable userSettings;
@observable projects;
@observable tasks = [];
@observable changeset = {
project: null,
task: null,
hours: '',
description: ''
};
componentDidMount() {
console.log(this.props.service)
const { settings } = this.props
this.apiClient = new ApiClient(settings)
this.fetchData()
window.addEventListener("keydown", this.handleKeyDown)
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleKeyDown)
}
open = _event => {
this.isOpen = true
};
close = _event => {
this.isOpen = false
};
fetchData = () => {
Promise.all([this.apiClient.login(), this.apiClient.projects()])
.then(responses => {
this.userSettings = get(responses, "[0].data")
this.projects = groupedProjectOptions(
get(responses, "[1].data.projects")
)
const {
last_project_id: lastProjectId,
last_task_id: lastTaskId
} = this.userSettings
this.changeset.project = findLastProject(lastProjectId)(this.projects)
this.changeset.task = findLastTask(lastTaskId)(this.changeset.project)
this.isLoading = false
})
.catch(console.error)
};
// EVENT HANDLERS -----------------------------------------------------------
handleKeyDown = event => {
if (event.keyCode === 27) {
this.close()
}
};
handleChange = event => {
const {
target: { name, value }
} = event
this.changeset[name] = value
if (name === "project") {
this.changeset.task = null
this.tasks = value.tasks
}
};
handleSubmit = event => {
event.preventDefault()
this.close()
};
// RENDER -------------------------------------------------------------------
render() {
if (this.isLoading) {
return null
}
return (
<>
<img
onClick={this.onOpen}
onClick={this.open}
src={chrome.extension.getURL(logoUrl)}
width="50%"
/>
{this.open && (
{this.isOpen && (
<Modal>
<Content>
<Form />
<button onClick={this.onClose}>Close</button>
<Form
projects={this.projects}
tasks={this.tasks}
changeset={this.changeset}
isLoading={this.isLoading}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
/>
</Content>
</Modal>
)}

View File

@@ -1,38 +1,83 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { login } from 'api/session'
import { observable } from 'mobx'
import { observer } from 'mobx-react'
import React, { Component } from "react"
import PropTypes from "prop-types"
import Select from "components/Select"
@observer
class Form extends Component {
@observable loading = true
static propTypes = {
inline: PropTypes.bool,
}
isLoading: PropTypes.bool.isRequired,
changeset: PropTypes.shape({
project: PropTypes.object,
task: PropTypes.object,
hours: PropTypes.string
}).isRequired,
projects: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired
};
static defaultProps = {
inline: true,
}
inline: true
};
componentDidMount() {
chrome.storage.sync.get(null, (store) => {
login(store.subdomain, store.api_key)
.then((response) => this.loading = false)
.catch((error) => console.log(error))
})
}
isValid = () => {
const { changeset } = this.props
return ["project", "task", "hours", "description"]
.map(prop => changeset[prop])
.every(Boolean)
};
// RENDER -------------------------------------------------------------------
render() {
if (this.loading) return null
if (this.isLoading) {
return null
}
const { projects, tasks, changeset, onChange, onSubmit } = this.props
return (
<div>
This is the Form {this.props.inline ? <span>INLINE</span> : <span>DEDICATED</span>}.
</div>
<form onSubmit={onSubmit}>
<div className="form-group">
<Select
name="project"
options={projects}
value={changeset.project}
onChange={onChange}
/>
</div>
<div className="form-group">
<Select
name="task"
options={tasks}
value={changeset.task}
onChange={onChange}
noOptionsMessage={() => "Zuerst Projekt wählen"}
/>
</div>
<div className="form-group">
<input
name="hours"
className="form-control"
onChange={onChange}
value={changeset.hours}
placeholder="0.00 h"
autoComplete="off"
autoFocus
/>
</div>
<div className="form-group">
<textarea
name="description"
onChange={onChange}
value={changeset.description}
placeholder="Beschreibung der Tätigkeit - mind. 3 Zeichen"
rows={4}
/>
</div>
<button disabled={!this.isValid()}>Speichern</button>
</form>
)
}
}

View File

@@ -0,0 +1,66 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ReactSelect, { createFilter } from "react-select"
import {
values,
isString,
isNumber,
join,
filter,
compose,
property
} from "lodash/fp"
const customTheme = theme => ({
...theme,
borderRadius: 0,
spacing: {
...theme.spacing,
baseUnit: 2,
controlHeight: 34
}
})
const customStyles = {
groupHeading: (base, _state) => ({
...base,
color: "black",
textTransform: "none",
fontWeight: "bold",
fontSize: "100%"
})
}
const filterOption = createFilter({
stringify: compose(
join(" "),
filter(value => isString(value) || isNumber(value)),
values,
property("data")
)
})
export default class Select extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
handleChange = value => {
const { name, onChange } = this.props
onChange({ target: { name, value } })
}
render() {
const passThroughProps = this.props
return (
<ReactSelect
{...passThroughProps}
onChange={this.handleChange}
filterOption={filterOption}
theme={customTheme}
styles={customStyles}
/>
)
}
}

View File

@@ -3,10 +3,10 @@ import ReactDOM from "react-dom"
import Bubble from "./components/Bubble"
import "../css/main.scss"
chrome.runtime.onMessage.addListener(({ type, service }) => {
chrome.runtime.onMessage.addListener(({ type, payload }) => {
switch (type) {
case "mountBubble": {
return mountBubble(service)
return mountBubble(payload)
}
case "unmountBubble": {
@@ -15,7 +15,7 @@ chrome.runtime.onMessage.addListener(({ type, service }) => {
}
})
const mountBubble = service => {
const mountBubble = ({ service, settings }) => {
if (document.getElementById("moco-bx-bubble")) {
return
}
@@ -28,7 +28,7 @@ const mountBubble = service => {
domBubble.setAttribute("id", "moco-bx-bubble")
document.body.appendChild(domBubble)
ReactDOM.render(createElement(Bubble, { service }), domBubble)
ReactDOM.render(createElement(Bubble, { service, settings }), domBubble)
}
const unmountBubble = () => {

57
src/js/utils.js Normal file
View File

@@ -0,0 +1,57 @@
import {
groupBy,
compose,
map,
toPairs,
flatMap,
pathEq,
get,
find,
curry
} from "lodash/fp"
const nilToArray = input => input || []
export const findLastProject = id =>
compose(
find(pathEq("value", id)),
flatMap(get("options"))
)
export const findLastTask = id =>
compose(
find(pathEq("value", id)),
get("tasks")
)
function taskOptions(tasks) {
return tasks.map(({ id, name }) => ({
label: name,
value: id
}))
}
export function projectOptions(projects) {
return projects.map(project => ({
value: project.id,
label: project.name,
customerName: project.customer_name,
tasks: taskOptions(project.tasks)
}))
}
export const groupedProjectOptions = compose(
map(([customerName, projects]) => ({
label: customerName,
options: projectOptions(projects)
})),
toPairs,
groupBy("customer_name"),
nilToArray
)
export const trace = curry((tag, value) => {
// eslint-disable-next-line no-console
console.log(tag, value)
return value
})