diff --git a/package.json b/package.json index 774a312..e821749 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "axios": "^0.18.0", + "lodash": "^4.17.11", "mobx": "^5.5.0", "mobx-react": "^5.2.8", "prop-types": "^15.6.2", "react": "^16.8.0", "react-dom": "^16.8.0", + "react-select": "^2.3.0", "route-parser": "^0.0.5" }, "devDependencies": { @@ -38,6 +40,7 @@ "html-webpack-plugin": "^3.2.0", "jest": "^24.1.0", "node-sass": "^4.11.0", + "prettier": "^1.16.4", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "webpack": "^4.15.0", diff --git a/src/css/_form.scss b/src/css/_form.scss index f86e7f8..497f510 100644 --- a/src/css/_form.scss +++ b/src/css/_form.scss @@ -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; + } } } diff --git a/src/js/api/Client.js b/src/js/api/Client.js index 5a0e096..afafbc8 100644 --- a/src/js/api/Client.js +++ b/src/js/api/Client.js @@ -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() diff --git a/src/js/api/projects.js b/src/js/api/projects.js deleted file mode 100644 index bcb7521..0000000 --- a/src/js/api/projects.js +++ /dev/null @@ -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 }) -} diff --git a/src/js/api/session.js b/src/js/api/session.js deleted file mode 100644 index f85e93b..0000000 --- a/src/js/api/session.js +++ /dev/null @@ -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 }) -} diff --git a/src/js/background.js b/src/js/background.js index ca07fcd..2b0f2f8 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -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") diff --git a/src/js/components/Bubble.js b/src/js/components/Bubble.js index 6ac3e1f..5167599 100644 --- a/src/js/components/Bubble.js +++ b/src/js/components/Bubble.js @@ -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 ( <> - {this.open && ( + {this.isOpen && ( -
- + )} diff --git a/src/js/components/Form.js b/src/js/components/Form.js index 9f764f7..00cee45 100644 --- a/src/js/components/Form.js +++ b/src/js/components/Form.js @@ -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 ( -
- This is the Form {this.props.inline ? INLINE : DEDICATED}. -
+ +
+ "Zuerst Projekt wählen"} + /> +
+
+ +
+
+