Draft Documents (#518)

* Mostly there

* Fix up specs

* Working scope, updated tests

* Don't record view on draft

* PR feedback

* Highlight drafts nav item

* Bugaboos

* Styling

* Refactoring, gradually addressing Jori feedback

* Show collection in drafts list
Flow fixes

* Ensure menu actions are hidden when draft
This commit is contained in:
Tom Moor
2018-02-27 22:41:12 -08:00
committed by GitHub
parent 79a0272230
commit 9142d975df
30 changed files with 519 additions and 194 deletions

View File

@@ -1,5 +1,5 @@
// @flow
import React, { Component } from 'react';
import * as React from 'react';
import get from 'lodash/get';
import styled from 'styled-components';
import { observable } from 'mobx';
@@ -13,25 +13,20 @@ import {
updateDocumentUrl,
documentMoveUrl,
documentEditUrl,
documentNewUrl,
matchDocumentEdit,
matchDocumentMove,
} from 'utils/routeHelpers';
import Document from 'models/Document';
import Actions from './components/Actions';
import DocumentMove from './components/DocumentMove';
import UiStore from 'stores/UiStore';
import DocumentsStore from 'stores/DocumentsStore';
import CollectionsStore from 'stores/CollectionsStore';
import DocumentMenu from 'menus/DocumentMenu';
import SaveAction from './components/SaveAction';
import LoadingPlaceholder from 'components/LoadingPlaceholder';
import LoadingIndicator from 'components/LoadingIndicator';
import Collaborators from 'components/Collaborators';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
import NewDocumentIcon from 'components/Icon/NewDocumentIcon';
import Actions, { Action, Separator } from 'components/Actions';
import Search from 'scenes/Search';
const DISCARD_CHANGES = `
@@ -50,7 +45,7 @@ type Props = {
};
@observer
class DocumentScene extends Component {
class DocumentScene extends React.Component {
props: Props;
savedTimeout: number;
@@ -59,6 +54,7 @@ class DocumentScene extends Component {
@observable newDocument: ?Document;
@observable isLoading = false;
@observable isSaving = false;
@observable isPublishing = false;
@observable notFound = false;
@observable moveModalOpen: boolean = false;
@@ -116,7 +112,9 @@ class DocumentScene extends Component {
// Cache data if user enters edit mode and cancels
this.editCache = document.text;
if (!this.isEditing) document.view();
if (!this.isEditing && document.publishedAt) {
document.view();
}
// Update url to match the current one
this.props.history.replace(
@@ -151,28 +149,21 @@ class DocumentScene extends Component {
return this.getDocument();
}
onClickEdit = () => {
if (!this.document) return;
this.props.history.push(documentEditUrl(this.document));
};
onClickNew = () => {
if (!this.document) return;
this.props.history.push(documentNewUrl(this.document));
};
handleCloseMoveModal = () => (this.moveModalOpen = false);
handleOpenMoveModal = () => (this.moveModalOpen = true);
onSave = async (redirect: boolean = false) => {
if (this.document && !this.document.allowSave) return;
this.editCache = null;
let document = this.document;
onSave = async (options: { redirect?: boolean, publish?: boolean } = {}) => {
const { redirect, publish } = options;
if (!document) return;
let document = this.document;
if (!document || !document.allowSave) return;
this.editCache = null;
this.isSaving = true;
document = await document.save();
this.isPublishing = publish;
document = await document.save(publish);
this.isSaving = false;
this.isPublishing = false;
if (redirect) {
this.props.history.push(document.url);
@@ -215,7 +206,6 @@ class DocumentScene extends Component {
render() {
const Editor = this.editorComponent;
const isNew = this.props.newDocument;
const isMoving = this.props.match.path === matchDocumentMove;
const document = this.document;
const titleText =
@@ -253,47 +243,19 @@ class DocumentScene extends Component {
onCancel={this.onDiscard}
readOnly={!this.isEditing}
/>
<Actions
align="center"
justify="flex-end"
readOnly={!this.isEditing}
>
{!isNew &&
!this.isEditing && <Collaborators document={document} />}
<Action>
{this.isEditing ? (
<SaveAction
isSaving={this.isSaving}
onClick={this.onSave.bind(this, true)}
disabled={
!(this.document && this.document.allowSave) ||
this.isSaving
}
isNew={!!isNew}
/>
) : (
<a onClick={this.onClickEdit}>Edit</a>
)}
</Action>
{this.isEditing && (
<Action>
<a onClick={this.onDiscard}>Discard</a>
</Action>
)}
{!this.isEditing && (
<Action>
<DocumentMenu document={document} />
</Action>
)}
{!this.isEditing && <Separator />}
<Action>
{!this.isEditing && (
<a onClick={this.onClickNew}>
<NewDocumentIcon />
</a>
)}
</Action>
</Actions>
{document && (
<Actions
document={document}
isDraft={!document.publishedAt}
isEditing={this.isEditing}
isSaving={this.isSaving}
isPublishing={this.isPublishing}
savingIsDisabled={!document.allowSave}
history={this.props.history}
onDiscard={this.onDiscard}
onSave={this.onSave}
/>
)}
</Flex>
)}
</Container>

View File

@@ -0,0 +1,133 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { color } from 'shared/styles/constants';
import Document from 'models/Document';
import { documentEditUrl, documentNewUrl } from 'utils/routeHelpers';
import DocumentMenu from 'menus/DocumentMenu';
import Collaborators from 'components/Collaborators';
import NewDocumentIcon from 'components/Icon/NewDocumentIcon';
import Actions, { Action, Separator } from 'components/Actions';
type Props = {
document: Document,
isDraft: boolean,
isEditing: boolean,
isSaving: boolean,
isPublishing: boolean,
savingIsDisabled: boolean,
onDiscard: () => *,
onSave: ({
redirect?: boolean,
publish?: boolean,
}) => *,
history: Object,
};
class DocumentActions extends React.Component {
props: Props;
handleNewDocument = () => {
this.props.history.push(documentNewUrl(this.props.document));
};
handleEdit = () => {
this.props.history.push(documentEditUrl(this.props.document));
};
handleSave = () => {
this.props.onSave({ redirect: true });
};
handlePublish = () => {
this.props.onSave({ redirect: true, publish: true });
};
render() {
const {
document,
isEditing,
isDraft,
isPublishing,
isSaving,
savingIsDisabled,
} = this.props;
return (
<Actions align="center" justify="flex-end" readOnly={!isEditing}>
{!isDraft && !isEditing && <Collaborators document={document} />}
{isDraft && (
<Action>
<Link
onClick={this.handlePublish}
title="Publish document (Cmd+Enter)"
disabled={savingIsDisabled}
highlight
>
{isPublishing ? 'Publishing…' : 'Publish'}
</Link>
</Action>
)}
{isEditing && (
<React.Fragment>
<Action>
<Link
onClick={this.handleSave}
title="Save changes (Cmd+Enter)"
disabled={savingIsDisabled}
isSaving={isSaving}
highlight={!isDraft}
>
{isSaving && !isPublishing ? 'Saving…' : 'Save'}
</Link>
</Action>
{isDraft && <Separator />}
</React.Fragment>
)}
{!isEditing && (
<Action>
<a onClick={this.handleEdit}>Edit</a>
</Action>
)}
{isEditing && (
<Action>
<a onClick={this.props.onDiscard}>
{document.hasPendingChanges ? 'Discard' : 'Done'}
</a>
</Action>
)}
{!isEditing && (
<Action>
<DocumentMenu document={document} />
</Action>
)}
{!isEditing &&
!isDraft && (
<React.Fragment>
<Separator />
<Action>
<a onClick={this.handleNewDocument}>
<NewDocumentIcon />
</a>
</Action>
</React.Fragment>
)}
</Actions>
);
}
}
const Link = styled.a`
display: flex;
align-items: center;
font-weight: ${props => (props.highlight ? 500 : 'inherit')};
color: ${props =>
props.highlight ? `${color.primary} !important` : 'inherit'};
opacity: ${props => (props.disabled ? 0.5 : 1)};
pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
`;
export default DocumentActions;

View File

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

View File

@@ -1,47 +0,0 @@
// @flow
import React from 'react';
import styled from 'styled-components';
type Props = {
onClick: (redirect: ?boolean) => *,
disabled?: boolean,
isNew?: boolean,
isSaving?: boolean,
};
class SaveAction extends React.Component {
props: Props;
onClick = (ev: MouseEvent) => {
if (this.props.disabled) return;
ev.preventDefault();
this.props.onClick();
};
render() {
const { isSaving, isNew, disabled } = this.props;
return (
<Link
onClick={this.onClick}
title="Save changes (Cmd+Enter)"
disabled={disabled}
>
{isNew
? isSaving ? 'Publishing…' : 'Publish'
: isSaving ? 'Saving…' : 'Save'}
</Link>
);
}
}
const Link = styled.a`
display: flex;
align-items: center;
opacity: ${props => (props.disabled ? 0.5 : 1)};
pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
`;
export default SaveAction;

View File

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

View File

@@ -0,0 +1,38 @@
// @flow
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import Empty from 'components/Empty';
import PageTitle from 'components/PageTitle';
import DocumentList from 'components/DocumentList';
import DocumentsStore from 'stores/DocumentsStore';
@observer
class Drafts extends Component {
props: {
documents: DocumentsStore,
};
componentDidMount() {
this.props.documents.fetchDrafts();
}
render() {
const { isLoaded, isFetching, drafts } = this.props.documents;
const showLoading = !isLoaded && isFetching;
const showEmpty = isLoaded && !drafts.length;
return (
<CenteredContent column auto>
<PageTitle title="Drafts" />
<h1>Drafts</h1>
{showLoading && <ListPlaceholder />}
{showEmpty && <Empty>No drafts yet.</Empty>}
<DocumentList documents={drafts} showCollection />
</CenteredContent>
);
}
}
export default inject('documents')(Drafts);

View File

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