diff --git a/.circleci/config.yml b/.circleci/config.yml
index cbc6f28e7..856014b43 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,4 +34,7 @@ jobs:
command: yarn test
- run:
name: lint
- command: yarn lint
\ No newline at end of file
+ command: yarn lint
+ - run:
+ name: flow
+ command: yarn flow
\ No newline at end of file
diff --git a/.githooks/pre-commit/flow.sh b/.githooks/pre-commit/flow.sh
index 1cb551e61..4d946a70d 100644
--- a/.githooks/pre-commit/flow.sh
+++ b/.githooks/pre-commit/flow.sh
@@ -1 +1 @@
-yarn lint:flow
\ No newline at end of file
+yarn flow
\ No newline at end of file
diff --git a/app/components/Button.js b/app/components/Button.js
index 35d6f5625..bd546e2ff 100644
--- a/app/components/Button.js
+++ b/app/components/Button.js
@@ -52,7 +52,8 @@ const RealButton = styled.button`
`} ${props =>
props.danger &&
`
- background: ${props.theme.danger};
+ background: ${props.theme.danger};
+ color: ${props.theme.white};
&:hover {
background: ${darken(0.05, props.theme.danger)};
diff --git a/app/components/DocumentList.js b/app/components/DocumentList.js
index d6e8861f3..98f9f41d0 100644
--- a/app/components/DocumentList.js
+++ b/app/components/DocumentList.js
@@ -6,17 +6,10 @@ import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
type Props = {
documents: Document[],
- showCollection?: boolean,
- showPublished?: boolean,
limit?: number,
};
-export default function DocumentList({
- limit,
- showCollection,
- showPublished,
- documents,
-}: Props) {
+export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents;
return (
@@ -25,12 +18,7 @@ export default function DocumentList({
defaultActiveChildIndex={0}
>
{items.map(document => (
-
+
))}
);
diff --git a/app/components/DocumentPreview/DocumentPreview.js b/app/components/DocumentPreview/DocumentPreview.js
index 95850e4f9..e41806fc3 100644
--- a/app/components/DocumentPreview/DocumentPreview.js
+++ b/app/components/DocumentPreview/DocumentPreview.js
@@ -17,6 +17,7 @@ type Props = {
context?: ?string,
showCollection?: boolean,
showPublished?: boolean,
+ link?: boolean,
ref?: *,
};
@@ -138,6 +139,7 @@ class DocumentPreview extends React.Component {
showPublished,
highlight,
context,
+ link,
...rest
} = this.props;
@@ -147,23 +149,29 @@ class DocumentPreview extends React.Component {
return (
- {!document.isDraft && (
-
- {document.starred ? (
-
- ) : (
-
- )}
-
- )}
+ {!document.isDraft &&
+ !document.isArchived && (
+
+ {document.starred ? (
+
+ ) : (
+
+ )}
+
+ )}
{!queryIsInTitle && (
diff --git a/app/components/DocumentPreview/components/PublishingInfo.js b/app/components/DocumentPreview/components/PublishingInfo.js
index 3425667dd..b8a553216 100644
--- a/app/components/DocumentPreview/components/PublishingInfo.js
+++ b/app/components/DocumentPreview/components/PublishingInfo.js
@@ -30,30 +30,49 @@ function PublishingInfo({ collection, showPublished, document }: Props) {
updatedAt,
updatedBy,
publishedAt,
+ archivedAt,
+ deletedAt,
isDraft,
} = document;
const neverUpdated = publishedAt === updatedAt;
+ let content;
+
+ if (deletedAt) {
+ content = (
+
+ deleted ago
+
+ );
+ } else if (archivedAt) {
+ content = (
+
+ archived ago
+
+ );
+ } else if (publishedAt && (neverUpdated || showPublished)) {
+ content = (
+
+ published ago
+
+ );
+ } else if (isDraft) {
+ content = (
+
+ saved ago
+
+ );
+ } else {
+ content = (
+
+ updated ago
+
+ );
+ }
return (
- {publishedAt && (neverUpdated || showPublished) ? (
-
- {updatedBy.name} published ago
-
- ) : (
-
- {updatedBy.name}
- {isDraft ? (
-
- saved ago
-
- ) : (
-
- updated ago
-
- )}
-
- )}
+ {updatedBy.name}
+ {content}
{collection && (
in {isDraft ? 'Drafts' : collection.name}
diff --git a/app/components/Layout.js b/app/components/Layout.js
index e7ef2c1b2..68f5e44cb 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -10,7 +10,6 @@ import keydown from 'react-keydown';
import Analytics from 'components/Analytics';
import Flex from 'shared/components/Flex';
import {
- documentEditUrl,
homeUrl,
searchUrl,
matchDocumentSlug as slug,
@@ -72,16 +71,6 @@ class Layout extends React.Component {
this.redirectTo = homeUrl();
}
- @keydown('e')
- goToEdit(ev) {
- const activeDocument = this.props.documents.active;
- if (!activeDocument) return;
-
- ev.preventDefault();
- ev.stopPropagation();
- this.redirectTo = documentEditUrl(activeDocument);
- }
-
@keydown('shift+/')
openKeyboardShortcuts() {
this.props.ui.setActiveModal('keyboard-shortcuts');
diff --git a/app/components/PaginatedDocumentList.js b/app/components/PaginatedDocumentList.js
index 39e63694f..20f11b84d 100644
--- a/app/components/PaginatedDocumentList.js
+++ b/app/components/PaginatedDocumentList.js
@@ -10,11 +10,10 @@ import DocumentList from 'components/DocumentList';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
type Props = {
- showCollection?: boolean,
- showPublished?: boolean,
documents: Document[],
fetch: (options: ?Object) => Promise<*>,
options?: Object,
+ heading?: React.Node,
empty?: React.Node,
};
@@ -66,25 +65,25 @@ class PaginatedDocumentList extends React.Component {
};
render() {
- const { showCollection, showPublished, empty, documents } = this.props;
+ const { empty, heading, documents, fetch, options, ...rest } = this.props;
+ const showLoading = !this.isLoaded && this.isFetching && !documents.length;
+ const showEmpty = this.isLoaded && !documents.length;
- return this.isLoaded || documents.length ? (
+ return (
- {documents.length ? (
-
- ) : (
+ {showEmpty ? (
empty
+ ) : (
+
+ {heading}
+
+ {this.allowLoadMore && (
+
+ )}
+
)}
- {this.allowLoadMore && (
-
- )}
+ {showLoading && }
- ) : (
-
);
}
}
diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js
index b695fed34..22384a8e1 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -1,7 +1,13 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
-import { HomeIcon, EditIcon, SearchIcon, StarredIcon } from 'outline-icons';
+import {
+ ArchiveIcon,
+ HomeIcon,
+ EditIcon,
+ SearchIcon,
+ StarredIcon,
+} from 'outline-icons';
import Flex from 'shared/components/Flex';
import AccountMenu from 'menus/AccountMenu';
@@ -94,6 +100,17 @@ class MainSidebar extends React.Component {
+
+ }
+ exact={false}
+ label="Archive"
+ active={
+ documents.active ? documents.active.isArchived : undefined
+ }
+ />
+
diff --git a/app/menus/AccountMenu.js b/app/menus/AccountMenu.js
index a2dec29f8..84d01bb9f 100644
--- a/app/menus/AccountMenu.js
+++ b/app/menus/AccountMenu.js
@@ -1,7 +1,5 @@
// @flow
import * as React from 'react';
-import { Redirect } from 'react-router-dom';
-import { observable } from 'mobx';
import { inject, observer } from 'mobx-react';
import { MoonIcon } from 'outline-icons';
import styled, { withTheme } from 'styled-components';
@@ -15,6 +13,7 @@ import {
githubIssuesUrl,
mailToUrl,
spectrumUrl,
+ settings,
} from '../../shared/utils/routeHelpers';
type Props = {
@@ -26,26 +25,15 @@ type Props = {
@observer
class AccountMenu extends React.Component {
- @observable redirectTo: ?string;
-
- componentDidUpdate() {
- this.redirectTo = undefined;
- }
-
handleOpenKeyboardShortcuts = () => {
this.props.ui.setActiveModal('keyboard-shortcuts');
};
- handleOpenSettings = () => {
- this.redirectTo = '/settings';
- };
-
handleLogout = () => {
this.props.auth.logout();
};
render() {
- if (this.redirectTo) return ;
const { ui, theme } = this.props;
const isLightTheme = ui.theme === 'light';
@@ -54,9 +42,7 @@ class AccountMenu extends React.Component {
style={{ marginRight: 10, marginTop: -10 }}
label={this.props.label}
>
-
- Settings
-
+ Settings
Keyboard shortcuts
@@ -77,8 +63,6 @@ class AccountMenu extends React.Component {
Report a bug
- Logout
-
Night Mode{' '}
@@ -87,6 +71,8 @@ class AccountMenu extends React.Component {
/>
+
+ Logout
);
}
diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js
index 92f92c8bf..249e8ee97 100644
--- a/app/menus/DocumentMenu.js
+++ b/app/menus/DocumentMenu.js
@@ -51,7 +51,20 @@ class DocumentMenu extends React.Component {
handleDuplicate = async (ev: SyntheticEvent<*>) => {
const duped = await this.props.document.duplicate();
+
+ // when duplicating, go straight to the duplicated document content
this.redirectTo = duped.url;
+ this.props.ui.showToast('Document duplicated');
+ };
+
+ handleArchive = async (ev: SyntheticEvent<*>) => {
+ await this.props.document.archive();
+ this.props.ui.showToast('Document archived');
+ };
+
+ handleRestore = async (ev: SyntheticEvent<*>) => {
+ await this.props.document.restore();
+ this.props.ui.showToast('Document restored');
};
handlePin = (ev: SyntheticEvent<*>) => {
@@ -87,9 +100,22 @@ class DocumentMenu extends React.Component {
const { document, label, className, showPrint, auth } = this.props;
const canShareDocuments = auth.team && auth.team.sharing;
+ if (document.isArchived) {
+ return (
+ } className={className}>
+
+ Restore
+
+
+ Delete…
+
+
+ );
+ }
+
return (
} className={className}>
- {!document.isDraft && (
+ {!document.isDraft ? (
{document.pinned ? (
@@ -128,10 +154,19 @@ class DocumentMenu extends React.Component {
Duplicate
+
+ Archive
+
+
+ Delete…
+
Move…
+ ) : (
+
+ Delete…
+
)}
- Delete…
Download
diff --git a/app/menus/NewChildDocumentMenu.js b/app/menus/NewChildDocumentMenu.js
index f0966d512..5dda4ef09 100644
--- a/app/menus/NewChildDocumentMenu.js
+++ b/app/menus/NewChildDocumentMenu.js
@@ -41,14 +41,14 @@ class NewChildDocumentMenu extends React.Component {
return (
} {...rest}>
-
- New child document
-
New document in {collection.name}
+
+ New child document
+
);
}
diff --git a/app/models/Document.js b/app/models/Document.js
index e6e593321..743958394 100644
--- a/app/models/Document.js
+++ b/app/models/Document.js
@@ -36,6 +36,8 @@ export default class Document extends BaseModel {
emoji: string;
parentDocument: ?string;
publishedAt: ?string;
+ archivedAt: string;
+ deletedAt: ?string;
url: string;
urlId: string;
shareUrl: ?string;
@@ -78,6 +80,16 @@ export default class Document extends BaseModel {
return [];
}
+ @computed
+ get isArchived(): boolean {
+ return !!this.archivedAt;
+ }
+
+ @computed
+ get isDeleted(): boolean {
+ return !!this.deletedAt;
+ }
+
@computed
get isDraft(): boolean {
return !this.publishedAt;
@@ -115,7 +127,11 @@ export default class Document extends BaseModel {
this.updateTitle();
};
- restore = (revision: Revision) => {
+ archive = () => {
+ return this.store.archive(this);
+ };
+
+ restore = (revision: ?Revision) => {
return this.store.restore(this, revision);
};
diff --git a/app/routes.js b/app/routes.js
index 6c9002eb6..f713bbb54 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -5,6 +5,7 @@ import Home from 'scenes/Home';
import Dashboard from 'scenes/Dashboard';
import Starred from 'scenes/Starred';
import Drafts from 'scenes/Drafts';
+import Archive from 'scenes/Archive';
import Collection from 'scenes/Collection';
import Document from 'scenes/Document';
import KeyedDocument from 'scenes/Document/KeyedDocument';
@@ -45,6 +46,7 @@ export default function Routes() {
+
diff --git a/app/scenes/Archive.js b/app/scenes/Archive.js
new file mode 100644
index 000000000..68053bcd3
--- /dev/null
+++ b/app/scenes/Archive.js
@@ -0,0 +1,38 @@
+// @flow
+import * as React from 'react';
+import { observer, inject } from 'mobx-react';
+
+import CenteredContent from 'components/CenteredContent';
+import Empty from 'components/Empty';
+import PageTitle from 'components/PageTitle';
+import Heading from 'components/Heading';
+import PaginatedDocumentList from 'components/PaginatedDocumentList';
+import Subheading from 'components/Subheading';
+import DocumentsStore from 'stores/DocumentsStore';
+
+type Props = {
+ documents: DocumentsStore,
+};
+
+@observer
+class Archive extends React.Component {
+ render() {
+ const { documents } = this.props;
+
+ return (
+
+
+ Archive
+ Documents}
+ empty={The document archive is empty at the moment.}
+ showCollection
+ />
+
+ );
+ }
+}
+
+export default inject('documents')(Archive);
diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js
index 713306e60..16163fafc 100644
--- a/app/scenes/Document/Document.js
+++ b/app/scenes/Document/Document.js
@@ -27,6 +27,8 @@ import LoadingPlaceholder from 'components/LoadingPlaceholder';
import LoadingIndicator from 'components/LoadingIndicator';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
+import Notice from 'shared/components/Notice';
+import Time from 'shared/components/Time';
import Search from 'scenes/Search';
import Error404 from 'scenes/Error404';
import ErrorOffline from 'scenes/ErrorOffline';
@@ -98,7 +100,19 @@ class DocumentScene extends React.Component {
@keydown('m')
goToMove(ev) {
ev.preventDefault();
- if (this.document) this.props.history.push(documentMoveUrl(this.document));
+
+ if (this.document && !this.document.isArchived) {
+ this.props.history.push(documentMoveUrl(this.document));
+ }
+ }
+
+ @keydown('e')
+ goToEdit(ev) {
+ ev.preventDefault();
+
+ if (this.document && !this.document.isArchived) {
+ this.props.history.push(documentEditUrl(this.document));
+ }
}
@keydown('esc')
@@ -156,6 +170,10 @@ class DocumentScene extends React.Component {
if (document) {
this.props.ui.setActiveDocument(document);
+ if (document.isArchived && this.isEditing) {
+ return this.goToDocumentCanonical();
+ }
+
if (this.props.auth.user && !shareId) {
if (!this.isEditing && document.publishedAt) {
this.viewTimeout = setTimeout(document.view, MARK_AS_VIEWED_AFTER);
@@ -200,10 +218,6 @@ class DocumentScene extends React.Component {
handleCloseMoveModal = () => (this.moveModalOpen = false);
handleOpenMoveModal = () => (this.moveModalOpen = true);
- onSaveAndExit = () => {
- this.onSave({ done: true });
- };
-
onSave = async (
options: { done?: boolean, publish?: boolean, autosave?: boolean } = {}
) => {
@@ -366,7 +380,13 @@ class DocumentScene extends React.Component {
onSave={this.onSave}
/>
)}
-
+
+ {document.archivedAt && (
+
+ Archived by {document.updatedBy.name}{' '}
+ ago
+
+ )}
{
onImageUploadStop={this.onImageUploadStop}
onSearchLink={this.onSearchLink}
onChange={this.onChange}
- onSave={this.onSaveAndExit}
+ onSave={this.onSave}
onCancel={this.onDiscard}
- readOnly={!this.isEditing}
+ readOnly={!this.isEditing || document.isArchived}
toc={!revision}
ui={this.props.ui}
schema={schema}
@@ -394,6 +414,8 @@ class DocumentScene extends React.Component {
}
const MaxWidth = styled(Flex)`
+ ${props =>
+ props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
padding: 0 16px;
max-width: 100vw;
width: 100%;
diff --git a/app/scenes/Document/components/Header.js b/app/scenes/Document/components/Header.js
index a25272c6e..a81dc6cde 100644
--- a/app/scenes/Document/components/Header.js
+++ b/app/scenes/Document/components/Header.js
@@ -20,6 +20,7 @@ import NewChildDocumentMenu from 'menus/NewChildDocumentMenu';
import DocumentShare from 'scenes/DocumentShare';
import Button from 'components/Button';
import Modal from 'components/Modal';
+import Badge from 'components/Badge';
import Collaborators from 'components/Collaborators';
import { Action, Separator } from 'components/Actions';
@@ -100,8 +101,10 @@ class Header extends React.Component {
savingIsDisabled,
auth,
} = this.props;
- const canShareDocuments = auth.team && auth.team.sharing;
+ const canShareDocuments =
+ auth.team && auth.team.sharing && !document.isArchived;
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
+ const canEdit = !document.isArchived && !isEditing;
return (
{
- {document.title}
+ {document.title} {document.isArchived && Archived}
{!isDraft && !isEditing && }
@@ -175,7 +178,7 @@ class Header extends React.Component {
)}
- {!isEditing && (
+ {canEdit && (
)}
- {!isEditing &&
+ {canEdit &&
!isDraft && (
@@ -249,6 +252,7 @@ const Title = styled.div`
font-size: 16px;
font-weight: 600;
text-align: center;
+ align-items: center;
justify-content: center;
text-overflow: ellipsis;
white-space: nowrap;
@@ -260,7 +264,7 @@ const Title = styled.div`
width: 0;
${breakpoint('tablet')`
- display: block;
+ display: flex;
flex-grow: 1;
`};
`;
diff --git a/app/scenes/DocumentDelete.js b/app/scenes/DocumentDelete.js
index 359d51196..ec0eb97c9 100644
--- a/app/scenes/DocumentDelete.js
+++ b/app/scenes/DocumentDelete.js
@@ -29,7 +29,9 @@ class DocumentDelete extends React.Component {
try {
await this.props.document.delete();
- this.props.history.push(collection.url);
+ if (this.props.ui.activeDocumentId === this.props.document.id) {
+ this.props.history.push(collection.url);
+ }
this.props.onSubmit();
} catch (err) {
this.props.ui.showToast(err.message);
@@ -46,9 +48,16 @@ class DocumentDelete extends React.Component {