Move document improvements (#927)

* Show all collections in UI

* Introduce command pattern

* Actually remove from previous collection

* Stash

* Fixes: Promises resolved outside of response lifecycle

* 💚

* 💚

* documentMover tests

* Transaction

* Perf. More in transactions
This commit is contained in:
Tom Moor
2019-04-08 21:25:13 -07:00
committed by GitHub
parent 16066c0b24
commit 763f57a3dc
16 changed files with 313 additions and 146 deletions

View File

@@ -2,16 +2,18 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import { GoToIcon } from 'outline-icons';
import { GoToIcon, CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
import Flex from 'shared/components/Flex';
import Document from 'models/Document';
import Collection from 'models/Collection';
import type { DocumentPath } from 'stores/CollectionsStore';
type Props = {
result: DocumentPath,
document?: Document,
onSuccess?: *,
document?: ?Document,
collection: ?Collection,
onSuccess?: () => void,
ref?: *,
};
@@ -23,27 +25,28 @@ class PathToDocument extends React.Component<Props> {
if (!document) return;
if (result.type === 'document') {
await document.move(result.id);
} else if (
result.type === 'collection' &&
result.id === document.collection.id
) {
await document.move(null);
await document.move(result.collectionId, result.id);
} else {
throw new Error('Not implemented yet');
await document.move(result.collectionId, null);
}
if (onSuccess) onSuccess();
};
render() {
const { result, document, ref } = this.props;
const { result, collection, document, ref } = this.props;
const Component = document ? ResultWrapperLink : ResultWrapper;
if (!result) return <div />;
return (
<Component ref={ref} onClick={this.handleClick} href="" selectable>
{collection &&
(collection.private ? (
<PrivateCollectionIcon color={collection.color} />
) : (
<CollectionIcon color={collection.color} />
))}
{result.path
.map(doc => <Title key={doc.id}>{doc.title}</Title>)
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
@@ -64,7 +67,9 @@ const Title = styled.span`
text-overflow: ellipsis;
`;
const StyledGoToIcon = styled(GoToIcon)``;
const StyledGoToIcon = styled(GoToIcon)`
opacity: 0.25;
`;
const ResultWrapper = styled.div`
display: flex;

View File

@@ -24,6 +24,11 @@ export default class Collection extends BaseModel {
updatedAt: ?string;
url: string;
@computed
get isPrivate(): boolean {
return this.private;
}
@computed
get isEmpty(): boolean {
return this.documents.length === 0;

View File

@@ -234,8 +234,8 @@ export default class Document extends BaseModel {
}
};
move = (parentDocumentId: ?string) => {
return this.store.move(this, parentDocumentId);
move = (collectionId: string, parentDocumentId: ?string) => {
return this.store.move(this, collectionId, parentDocumentId);
};
duplicate = () => {

View File

@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
import { observable, computed } from 'mobx';
import { observer, inject } from 'mobx-react';
import { Search } from 'js-search';
import { first, last } from 'lodash';
import { last } from 'lodash';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import styled from 'styled-components';
@@ -33,19 +33,14 @@ class DocumentMove extends React.Component<Props> {
@computed
get searchIndex() {
const { document, collections } = this.props;
const { collections } = this.props;
const paths = collections.pathsToDocuments;
const index = new Search('id');
index.addIndex('title');
// Build index
const indexeableDocuments = [];
paths.forEach(path => {
// TMP: For now, exclude paths to other collections
if (first(path.path).id !== document.collection.id) return;
indexeableDocuments.push(path);
});
paths.forEach(path => indexeableDocuments.push(path));
index.addDocuments(indexeableDocuments);
return index;
@@ -63,23 +58,22 @@ class DocumentMove extends React.Component<Props> {
} else {
// Default results, root of the current collection
results = [];
document.collection.documents.forEach(doc => {
const path = collections.getPathForDocument(doc.id);
if (doc && path) {
results.push(path);
collections.orderedData.forEach(collection => {
collection.documents.forEach(doc => {
const path = collections.getPathForDocument(doc.id);
if (doc && path) {
results.push(path);
}
});
const rootPath = collections.getPathForDocument(collection.id);
if (rootPath) {
results = [rootPath, ...results];
}
});
}
}
if (document && document.parentDocumentId) {
// Add root if document does have a parent document
const rootPath = collections.getPathForDocument(document.collection.id);
if (rootPath) {
results = [rootPath, ...results];
}
}
// Exclude root from search results if document is already at the root
if (!document.parentDocumentId) {
results = results.filter(result => result.id !== document.collection.id);
@@ -119,7 +113,12 @@ class DocumentMove extends React.Component<Props> {
const result = collections.getPathForDocument(document.id);
if (result) {
return <PathToDocument result={result} />;
return (
<PathToDocument
result={result}
collection={collections.get(result.collectionId)}
/>
);
}
}
@@ -141,7 +140,7 @@ class DocumentMove extends React.Component<Props> {
<Labeled label="Choose a new location">
<Input
type="text"
placeholder="Filter by document name…"
placeholder="Filter…"
onKeyDown={this.handleKeyDown}
onChange={this.handleFilter}
required
@@ -158,6 +157,7 @@ class DocumentMove extends React.Component<Props> {
key={result.id}
result={result}
document={document}
collection={collections.get(result.collectionId)}
ref={ref =>
index === 0 && this.setFirstDocumentRef(ref)
}

View File

@@ -65,6 +65,10 @@ export default class BaseStore<T: BaseModel> {
return this.create(params);
}
get(id: string): ?T {
return this.data.get(id);
}
@action
async create(params: Object) {
if (!this.actions.includes('create')) {

View File

@@ -10,9 +10,10 @@ import naturalSort from 'shared/utils/naturalSort';
export type DocumentPathItem = {
id: string,
collectionId: string,
title: string,
url: string,
type: 'document' | 'collection',
type: 'collection' | 'document',
};
export type DocumentPath = DocumentPathItem & {
@@ -52,20 +53,26 @@ export default class CollectionsStore extends BaseStore<Collection> {
@computed
get pathsToDocuments(): DocumentPath[] {
let results = [];
const travelDocuments = (documentList, path) =>
const travelDocuments = (documentList, collectionId, path) =>
documentList.forEach(document => {
const { id, title, url } = document;
const node = { id, title, url, type: 'document' };
const node = { id, collectionId, title, url, type: 'document' };
results.push(concat(path, node));
travelDocuments(document.children, concat(path, [node]));
travelDocuments(document.children, collectionId, concat(path, [node]));
});
if (this.isLoaded) {
this.data.forEach(collection => {
const { id, name, url } = collection;
const node = { id, title: name, url, type: 'collection' };
const node = {
id,
collectionId: id,
title: name,
url,
type: 'collection',
};
results.push([node]);
travelDocuments(collection.documents, [node]);
travelDocuments(collection.documents, id, [node]);
});
}

View File

@@ -294,17 +294,20 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
move = async (document: Document, parentDocumentId: ?string) => {
move = async (
document: Document,
collectionId: string,
parentDocumentId: ?string
) => {
const res = await client.post('/documents.move', {
id: document.id,
parentDocument: parentDocumentId,
collectionId,
parentDocumentId,
});
invariant(res && res.data, 'Data not available');
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
return this.add(res.data);
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
};
@action