CollectionNew scene

This commit is contained in:
Tom Moor
2017-07-09 10:27:29 -07:00
parent 98da54d82a
commit f456dc6b6a
12 changed files with 164 additions and 72 deletions

View File

@ -13,11 +13,12 @@ import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import { LoadingIndicatorBar } from 'components/LoadingIndicator'; import { LoadingIndicatorBar } from 'components/LoadingIndicator';
import Scrollable from 'components/Scrollable'; import Scrollable from 'components/Scrollable';
import Avatar from 'components/Avatar'; import Avatar from 'components/Avatar';
import Modal from 'components/Modal';
import CollectionNew from 'scenes/CollectionNew';
import SidebarCollection from './components/SidebarCollection'; import SidebarCollection from './components/SidebarCollection';
import SidebarCollectionList from './components/SidebarCollectionList'; import SidebarCollectionList from './components/SidebarCollectionList';
import SidebarLink from './components/SidebarLink'; import SidebarLink from './components/SidebarLink';
import Modals from './components/Modals';
import UserStore from 'stores/UserStore'; import UserStore from 'stores/UserStore';
import AuthStore from 'stores/AuthStore'; import AuthStore from 'stores/AuthStore';
@ -39,6 +40,8 @@ type Props = {
@observer class Layout extends React.Component { @observer class Layout extends React.Component {
props: Props; props: Props;
state: { createCollectionModalOpen: boolean };
state = { createCollectionModalOpen: false };
static defaultProps = { static defaultProps = {
search: true, search: true,
@ -60,8 +63,12 @@ type Props = {
this.props.auth.logout(() => this.props.history.push('/')); this.props.auth.logout(() => this.props.history.push('/'));
}; };
createNewCollection = () => { handleCreateCollection = () => {
this.props.ui.openModal('NewCollection'); this.setState({ createCollectionModalOpen: true });
};
handleCloseModal = () => {
this.setState({ createCollectionModalOpen: false });
}; };
render() { render() {
@ -78,12 +85,6 @@ type Props = {
}, },
]} ]}
/> />
<Modals
name={this.props.ui.modalName}
component={this.props.ui.modalComponent}
onRequestClose={this.props.ui.closeModal}
{...this.props.ui.modalProps}
/>
{this.props.ui.progressBarVisible && <LoadingIndicatorBar />} {this.props.ui.progressBarVisible && <LoadingIndicatorBar />}
@ -122,7 +123,7 @@ type Props = {
<SidebarLink to="/dashboard">Home</SidebarLink> <SidebarLink to="/dashboard">Home</SidebarLink>
<SidebarLink to="/starred">Starred</SidebarLink> <SidebarLink to="/starred">Starred</SidebarLink>
</LinkSection> </LinkSection>
<a onClick={this.createNewCollection}> <a onClick={this.handleCreateCollection}>
Create new collection Create new collection
</a> </a>
<LinkSection> <LinkSection>
@ -142,6 +143,12 @@ type Props = {
{this.props.children} {this.props.children}
</Content> </Content>
</Flex> </Flex>
<Modal
isOpen={this.state.createCollectionModalOpen}
onRequestClose={this.handleCloseModal}
>
<CollectionNew />
</Modal>
</Container> </Container>
); );
} }

View File

@ -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 (
<Modal
isOpen={isOpen}
contentLabel={name}
onRequestClose={onRequestClose}
>
<button onClick={onRequestClose}>Close</button>
{isOpen && <ModalComponent {...rest} />}
</Modal>
);
}
}
export default Modals;

View File

@ -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 (
<ReactModal
contentLabel={title}
onRequestClose={onRequestClose}
{...rest}
>
<Header>
<button onClick={onRequestClose}>Close</button>
{title}
</Header>
{children}
</ReactModal>
);
}
}
const Header = styled.div`
text-align: center;
font-weight: semibold;
`;
export default Modal;

View File

@ -0,0 +1,3 @@
// @flow
import Modal from './Modal';
export default Modal;

View File

@ -1,5 +0,0 @@
// @flow
// All components wishing to be used as modals must be defined below
import NewCollection from './NewCollection';
export default { NewCollection };

View File

@ -3,12 +3,16 @@ import { extendObservable, action, computed, runInAction } from 'mobx';
import invariant from 'invariant'; import invariant from 'invariant';
import _ from 'lodash'; import _ from 'lodash';
import ApiClient, { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import stores from 'stores'; import stores from 'stores';
import ErrorsStore from 'stores/ErrorsStore'; import ErrorsStore from 'stores/ErrorsStore';
import type { NavigationNode } from 'types'; import type { NavigationNode } from 'types';
class Collection { class Collection {
isSaving: boolean = false;
hasPendingChanges: boolean = false;
errors: ErrorsStore;
createdAt: string; createdAt: string;
description: ?string; description: ?string;
id: string; id: string;
@ -18,9 +22,6 @@ class Collection {
updatedAt: string; updatedAt: string;
url: string; url: string;
client: ApiClient;
errors: ErrorsStore;
/* Computed */ /* Computed */
@computed get entryUrl(): string { @computed get entryUrl(): string {
@ -29,26 +30,59 @@ class Collection {
/* Actions */ /* Actions */
@action update = async () => { @action fetch = async () => {
try { 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'); invariant(res && res.data, 'API response should be available');
const { data } = res; const { data } = res;
runInAction('Collection#update', () => { runInAction('Collection#fetch', () => {
this.updateData(data); this.updateData(data);
}); });
} catch (e) { } catch (e) {
this.errors.add('Collection failed loading'); 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); extendObservable(this, data);
} }
constructor(collection: Collection) { constructor(collection: Object = {}) {
this.updateData(collection); this.updateData(collection);
this.client = client;
this.errors = stores.errors; this.errors = stores.errors;
} }
} }

View File

@ -30,6 +30,7 @@ class Document {
starred: boolean = false; starred: boolean = false;
text: string = ''; text: string = '';
title: string = 'Untitled document'; title: string = 'Untitled document';
parentDocument: ?Document;
updatedAt: string; updatedAt: string;
updatedBy: User; updatedBy: User;
url: string; url: string;
@ -151,13 +152,14 @@ class Document {
return this; return this;
}; };
updateData(data: Object | Document) { updateData(data: Object = {}) {
if (data.text) data.title = parseHeader(data.text); if (data.text) data.title = parseHeader(data.text);
data.hasPendingChanges = true;
extendObservable(this, data); extendObservable(this, data);
} }
constructor(document?: Object = {}) { constructor(data?: Object = {}) {
this.updateData(document); this.updateData(data);
this.errors = stores.errors; this.errors = stores.errors;
} }
} }

View File

@ -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 (
<form onSubmit={this.handleSubmit}>
{collection.errors.errors.map(error => <span>{error}</span>)}
<Input
type="text"
placeholder="Name"
onChange={this.handleNameChange}
value={collection.name}
autoFocus
/>
<Input
type="textarea"
placeholder="Description (optional)"
onChange={this.handleDescriptionChange}
value={collection.description}
/>
<Button type="submit" disabled={collection.isSaving}>
{collection.isSaving ? 'Creating…' : 'Create'}
</Button>
</form>
);
}
}
export default CollectionNew;

View File

@ -0,0 +1,3 @@
// @flow
import CollectionNew from './CollectionNew';
export default CollectionNew;

View File

@ -120,7 +120,7 @@ type Props = {
onChange = text => { onChange = text => {
if (!this.document) return; if (!this.document) return;
this.document.updateData({ text, hasPendingChanges: true }); this.document.updateData({ text });
}; };
onCancel = () => { onCancel = () => {

View File

@ -1,7 +1,7 @@
// @flow // @flow
import { observable, action } from 'mobx'; import { observable, action } from 'mobx';
class UiStore { class ErrorsStore {
@observable errors = observable.array([]); @observable errors = observable.array([]);
/* Actions */ /* Actions */
@ -15,4 +15,4 @@ class UiStore {
}; };
} }
export default UiStore; export default ErrorsStore;

View File

@ -2,14 +2,11 @@
import { observable, action, computed } from 'mobx'; import { observable, action, computed } from 'mobx';
import Document from 'models/Document'; import Document from 'models/Document';
import Collection from 'models/Collection'; import Collection from 'models/Collection';
import modals from 'components/modals';
class UiStore { class UiStore {
@observable activeDocument: ?Document; @observable activeDocument: ?Document;
@observable progressBarVisible: boolean = false; @observable progressBarVisible: boolean = false;
@observable editMode: boolean = false; @observable editMode: boolean = false;
@observable modalName: ?string;
@observable modalProps: ?Object;
/* Computed */ /* Computed */
@ -17,10 +14,6 @@ class UiStore {
return this.activeDocument ? this.activeDocument.collection : undefined; return this.activeDocument ? this.activeDocument.collection : undefined;
} }
@computed get modalComponent(): ?ReactClass<any> {
if (this.modalName) return modals[this.modalName];
}
/* Actions */ /* Actions */
@action setActiveDocument = (document: Document): void => { @action setActiveDocument = (document: Document): void => {
@ -31,16 +24,6 @@ class UiStore {
this.activeDocument = undefined; 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() { @action enableEditMode() {
this.editMode = true; this.editMode = true;
} }