diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index a92b587a..0fdf3d97 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -13,11 +13,12 @@ import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; import Scrollable from 'components/Scrollable'; import Avatar from 'components/Avatar'; +import Modal from 'components/Modal'; +import CollectionNew from 'scenes/CollectionNew'; import SidebarCollection from './components/SidebarCollection'; import SidebarCollectionList from './components/SidebarCollectionList'; import SidebarLink from './components/SidebarLink'; -import Modals from './components/Modals'; import UserStore from 'stores/UserStore'; import AuthStore from 'stores/AuthStore'; @@ -39,6 +40,8 @@ type Props = { @observer class Layout extends React.Component { props: Props; + state: { createCollectionModalOpen: boolean }; + state = { createCollectionModalOpen: false }; static defaultProps = { search: true, @@ -60,8 +63,12 @@ type Props = { this.props.auth.logout(() => this.props.history.push('/')); }; - createNewCollection = () => { - this.props.ui.openModal('NewCollection'); + handleCreateCollection = () => { + this.setState({ createCollectionModalOpen: true }); + }; + + handleCloseModal = () => { + this.setState({ createCollectionModalOpen: false }); }; render() { @@ -78,12 +85,6 @@ type Props = { }, ]} /> - {this.props.ui.progressBarVisible && } @@ -122,7 +123,7 @@ type Props = { Home Starred - + Create new collection @@ -142,6 +143,12 @@ type Props = { {this.props.children} + + + ); } diff --git a/frontend/components/Layout/components/Modals.js b/frontend/components/Layout/components/Modals.js deleted file mode 100644 index 891aa5ca..00000000 --- a/frontend/components/Layout/components/Modals.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import Modal from 'react-modal'; - -class Modals extends Component { - render() { - const { name, component, onRequestClose, ...rest } = this.props; - const isOpen = !!component; - const ModalComponent = component; - - return ( - - - {isOpen && } - - ); - } -} - -export default Modals; diff --git a/frontend/components/Modal/Modal.js b/frontend/components/Modal/Modal.js new file mode 100644 index 00000000..3735386d --- /dev/null +++ b/frontend/components/Modal/Modal.js @@ -0,0 +1,36 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import ReactModal from 'react-modal'; + +class Modal extends Component { + render() { + const { + children, + title = 'Untitled Modal', + onRequestClose, + ...rest + } = this.props; + + return ( + +
+ + {title} +
+ {children} +
+ ); + } +} + +const Header = styled.div` + text-align: center; + font-weight: semibold; +`; + +export default Modal; diff --git a/frontend/components/Modal/index.js b/frontend/components/Modal/index.js new file mode 100644 index 00000000..90759335 --- /dev/null +++ b/frontend/components/Modal/index.js @@ -0,0 +1,3 @@ +// @flow +import Modal from './Modal'; +export default Modal; diff --git a/frontend/components/modals.js b/frontend/components/modals.js deleted file mode 100644 index c409daae..00000000 --- a/frontend/components/modals.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow -// All components wishing to be used as modals must be defined below -import NewCollection from './NewCollection'; - -export default { NewCollection }; diff --git a/frontend/models/Collection.js b/frontend/models/Collection.js index adc663c4..bc1755cd 100644 --- a/frontend/models/Collection.js +++ b/frontend/models/Collection.js @@ -3,12 +3,16 @@ import { extendObservable, action, computed, runInAction } from 'mobx'; import invariant from 'invariant'; import _ from 'lodash'; -import ApiClient, { client } from 'utils/ApiClient'; +import { client } from 'utils/ApiClient'; import stores from 'stores'; import ErrorsStore from 'stores/ErrorsStore'; import type { NavigationNode } from 'types'; class Collection { + isSaving: boolean = false; + hasPendingChanges: boolean = false; + errors: ErrorsStore; + createdAt: string; description: ?string; id: string; @@ -18,9 +22,6 @@ class Collection { updatedAt: string; url: string; - client: ApiClient; - errors: ErrorsStore; - /* Computed */ @computed get entryUrl(): string { @@ -29,26 +30,59 @@ class Collection { /* Actions */ - @action update = async () => { + @action fetch = async () => { try { - const res = await this.client.post('/collections.info', { id: this.id }); + const res = await client.post('/collections.info', { id: this.id }); invariant(res && res.data, 'API response should be available'); const { data } = res; - runInAction('Collection#update', () => { + runInAction('Collection#fetch', () => { this.updateData(data); }); } catch (e) { this.errors.add('Collection failed loading'); } + + return this; }; - updateData(data: Collection) { + @action save = async () => { + if (this.isSaving) return this; + this.isSaving = true; + + try { + let res; + if (this.id) { + res = await client.post('/collections.update', { + id: this.id, + name: this.name, + description: this.description, + }); + } else { + res = await client.post('/collections.create', { + name: this.name, + description: this.description, + }); + } + invariant(res && res.data, 'Data should be available'); + this.updateData({ + ...res.data, + hasPendingChanges: false, + }); + } catch (e) { + this.errors.add('Collection failed saving'); + } finally { + this.isSaving = false; + } + + return this; + }; + + updateData(data: Object = {}) { extendObservable(this, data); } - constructor(collection: Collection) { + constructor(collection: Object = {}) { this.updateData(collection); - this.client = client; this.errors = stores.errors; } } diff --git a/frontend/models/Document.js b/frontend/models/Document.js index db35d742..37cca990 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -30,6 +30,7 @@ class Document { starred: boolean = false; text: string = ''; title: string = 'Untitled document'; + parentDocument: ?Document; updatedAt: string; updatedBy: User; url: string; @@ -151,13 +152,14 @@ class Document { return this; }; - updateData(data: Object | Document) { + updateData(data: Object = {}) { if (data.text) data.title = parseHeader(data.text); + data.hasPendingChanges = true; extendObservable(this, data); } - constructor(document?: Object = {}) { - this.updateData(document); + constructor(data?: Object = {}) { + this.updateData(data); this.errors = stores.errors; } } diff --git a/frontend/scenes/CollectionNew/CollectionNew.js b/frontend/scenes/CollectionNew/CollectionNew.js new file mode 100644 index 00000000..e49ff931 --- /dev/null +++ b/frontend/scenes/CollectionNew/CollectionNew.js @@ -0,0 +1,53 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import Button from 'components/Button'; +import Input from 'components/Input'; +import Collection from 'models/Collection'; + +@observer class CollectionNew extends Component { + static defaultProps = { + collection: new Collection(), + }; + + handleSubmit = async (ev: SyntheticEvent) => { + ev.preventDefault(); + await this.props.collection.save(); + }; + + handleNameChange = (ev: SyntheticInputEvent) => { + this.props.collection.updateData({ name: ev.target.value }); + }; + + handleDescriptionChange = (ev: SyntheticInputEvent) => { + this.props.collection.updateData({ description: ev.target.value }); + }; + + render() { + const { collection } = this.props; + + return ( +
+ {collection.errors.errors.map(error => {error})} + + + +
+ ); + } +} + +export default CollectionNew; diff --git a/frontend/scenes/CollectionNew/index.js b/frontend/scenes/CollectionNew/index.js new file mode 100644 index 00000000..651d8d5c --- /dev/null +++ b/frontend/scenes/CollectionNew/index.js @@ -0,0 +1,3 @@ +// @flow +import CollectionNew from './CollectionNew'; +export default CollectionNew; diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index d04c08c5..bc104f2e 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -120,7 +120,7 @@ type Props = { onChange = text => { if (!this.document) return; - this.document.updateData({ text, hasPendingChanges: true }); + this.document.updateData({ text }); }; onCancel = () => { diff --git a/frontend/stores/ErrorsStore.js b/frontend/stores/ErrorsStore.js index abdc8eb6..02dc9739 100644 --- a/frontend/stores/ErrorsStore.js +++ b/frontend/stores/ErrorsStore.js @@ -1,7 +1,7 @@ // @flow import { observable, action } from 'mobx'; -class UiStore { +class ErrorsStore { @observable errors = observable.array([]); /* Actions */ @@ -15,4 +15,4 @@ class UiStore { }; } -export default UiStore; +export default ErrorsStore; diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js index 59ce9fdf..6345af8b 100644 --- a/frontend/stores/UiStore.js +++ b/frontend/stores/UiStore.js @@ -2,14 +2,11 @@ import { observable, action, computed } from 'mobx'; import Document from 'models/Document'; import Collection from 'models/Collection'; -import modals from 'components/modals'; class UiStore { @observable activeDocument: ?Document; @observable progressBarVisible: boolean = false; @observable editMode: boolean = false; - @observable modalName: ?string; - @observable modalProps: ?Object; /* Computed */ @@ -17,10 +14,6 @@ class UiStore { return this.activeDocument ? this.activeDocument.collection : undefined; } - @computed get modalComponent(): ?ReactClass { - if (this.modalName) return modals[this.modalName]; - } - /* Actions */ @action setActiveDocument = (document: Document): void => { @@ -31,16 +24,6 @@ class UiStore { this.activeDocument = undefined; }; - @action openModal = (name: string, props?: Object) => { - this.modalName = name; - this.modalProps = props; - }; - - @action closeModal = () => { - this.modalName = undefined; - this.modalProps = undefined; - }; - @action enableEditMode() { this.editMode = true; }