CollectionNew scene
This commit is contained in:
@ -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 = {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<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 />}
|
||||
|
||||
@ -122,7 +123,7 @@ type Props = {
|
||||
<SidebarLink to="/dashboard">Home</SidebarLink>
|
||||
<SidebarLink to="/starred">Starred</SidebarLink>
|
||||
</LinkSection>
|
||||
<a onClick={this.createNewCollection}>
|
||||
<a onClick={this.handleCreateCollection}>
|
||||
Create new collection
|
||||
</a>
|
||||
<LinkSection>
|
||||
@ -142,6 +143,12 @@ type Props = {
|
||||
{this.props.children}
|
||||
</Content>
|
||||
</Flex>
|
||||
<Modal
|
||||
isOpen={this.state.createCollectionModalOpen}
|
||||
onRequestClose={this.handleCloseModal}
|
||||
>
|
||||
<CollectionNew />
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
36
frontend/components/Modal/Modal.js
Normal file
36
frontend/components/Modal/Modal.js
Normal 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;
|
3
frontend/components/Modal/index.js
Normal file
3
frontend/components/Modal/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Modal from './Modal';
|
||||
export default Modal;
|
@ -1,5 +0,0 @@
|
||||
// @flow
|
||||
// All components wishing to be used as modals must be defined below
|
||||
import NewCollection from './NewCollection';
|
||||
|
||||
export default { NewCollection };
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
53
frontend/scenes/CollectionNew/CollectionNew.js
Normal file
53
frontend/scenes/CollectionNew/CollectionNew.js
Normal 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;
|
3
frontend/scenes/CollectionNew/index.js
Normal file
3
frontend/scenes/CollectionNew/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import CollectionNew from './CollectionNew';
|
||||
export default CollectionNew;
|
@ -120,7 +120,7 @@ type Props = {
|
||||
|
||||
onChange = text => {
|
||||
if (!this.document) return;
|
||||
this.document.updateData({ text, hasPendingChanges: true });
|
||||
this.document.updateData({ text });
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
|
@ -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;
|
||||
|
@ -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<any> {
|
||||
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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user