Load projects and initialize form with last project and task
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
#moco-bx-container {
|
||||
input {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
@@ -9,8 +13,10 @@
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.5rem 0.75rem;
|
||||
input, textarea {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: white;
|
||||
border-color: #cccccc;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -42,6 +48,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
input[name="hours"] {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
textarea[name="description"] {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
@@ -55,9 +70,14 @@
|
||||
border-color: #7dc332;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #639a28;
|
||||
border-color: #639a28;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
66
src/js/components/Select.js
Normal file
66
src/js/components/Select.js
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
57
src/js/utils.js
Normal 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
|
||||
})
|
||||
Reference in New Issue
Block a user