CollectionNew scene
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 => {
|
onChange = text => {
|
||||||
if (!this.document) return;
|
if (!this.document) return;
|
||||||
this.document.updateData({ text, hasPendingChanges: true });
|
this.document.updateData({ text });
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = () => {
|
onCancel = () => {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user