Bulk export (#684)

* First pass (working) collection export to zip

* Add export confirmation screen

* 👕

* Refactor

* Job for team export, move to tmp file, settings UI

* Export all collections job

* 👕

* Add specs

* Clarify UI
This commit is contained in:
Tom Moor
2018-06-20 21:33:21 -07:00
committed by GitHub
parent cedd31c9ea
commit b9e0668d7d
26 changed files with 543 additions and 28 deletions

View File

@@ -6,6 +6,7 @@ import UiStore from 'stores/UiStore';
import CollectionNew from 'scenes/CollectionNew';
import CollectionEdit from 'scenes/CollectionEdit';
import CollectionDelete from 'scenes/CollectionDelete';
import CollectionExport from 'scenes/CollectionExport';
import DocumentDelete from 'scenes/DocumentDelete';
import DocumentShare from 'scenes/DocumentShare';
import KeyboardShortcuts from 'scenes/KeyboardShortcuts';
@@ -45,6 +46,9 @@ class Modals extends React.Component<Props> {
<Modal name="collection-delete" title="Delete collection">
<CollectionDelete onSubmit={this.handleClose} />
</Modal>
<Modal name="collection-export" title="Export collection">
<CollectionExport onSubmit={this.handleClose} />
</Modal>
<Modal name="document-share" title="Share document">
<DocumentShare onSubmit={this.handleClose} />
</Modal>

View File

@@ -2,6 +2,7 @@
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import {
DocumentIcon,
ProfileIcon,
SettingsIcon,
CodeIcon,
@@ -74,6 +75,11 @@ class SettingsSidebar extends React.Component<Props> {
Integrations
</SidebarLink>
)}
{user.isAdmin && (
<SidebarLink to="/settings/export" icon={<DocumentIcon />}>
Export Data
</SidebarLink>
)}
</Section>
</Scrollable>
</Flex>

View File

@@ -28,6 +28,7 @@ import People from 'scenes/Settings/People';
import Slack from 'scenes/Settings/Slack';
import Shares from 'scenes/Settings/Shares';
import Tokens from 'scenes/Settings/Tokens';
import Export from 'scenes/Settings/Export';
import Error404 from 'scenes/Error404';
import ErrorBoundary from 'components/ErrorBoundary';
@@ -96,6 +97,11 @@ if (element) {
path="/settings/integrations/slack"
component={Slack}
/>
<Route
exact
path="/settings/export"
component={Export}
/>
<Route
exact
path="/collections/:id"

View File

@@ -61,6 +61,12 @@ class CollectionMenu extends React.Component<Props> {
this.props.ui.setActiveModal('collection-delete', { collection });
};
onExport = (ev: SyntheticEvent<*>) => {
ev.preventDefault();
const { collection } = this.props;
this.props.ui.setActiveModal('collection-export', { collection });
};
render() {
const { collection, label, onOpen, onClose } = this.props;
@@ -87,6 +93,9 @@ class CollectionMenu extends React.Component<Props> {
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={this.onEdit}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={this.onExport}>
Export
</DropdownMenuItem>
</React.Fragment>
)}
<DropdownMenuItem onClick={this.onDelete}>Delete</DropdownMenuItem>

View File

@@ -133,6 +133,11 @@ class Collection extends BaseModel {
return false;
};
@action
export = async () => {
await client.post('/collections.export', { id: this.id });
};
@action
updateData(data: Object = {}) {
this.data = data;

View File

@@ -0,0 +1,55 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { inject, observer } from 'mobx-react';
import Button from 'components/Button';
import Flex from 'shared/components/Flex';
import HelpText from 'components/HelpText';
import Collection from 'models/Collection';
import AuthStore from 'stores/AuthStore';
import UiStore from 'stores/UiStore';
type Props = {
collection: Collection,
auth: AuthStore,
ui: UiStore,
onSubmit: () => void,
};
@observer
class CollectionExport extends React.Component<Props> {
@observable isLoading: boolean = false;
handleSubmit = async (ev: SyntheticEvent<*>) => {
ev.preventDefault();
this.isLoading = true;
await this.props.collection.export();
this.isLoading = false;
this.props.ui.showToast('Export in progress…', 'success');
this.props.onSubmit();
};
render() {
const { collection, auth } = this.props;
if (!auth.user) return;
return (
<Flex column>
<form onSubmit={this.handleSubmit}>
<HelpText>
Exporting the collection <strong>{collection.name}</strong> may take
a few minutes. Well put together a zip file of your documents in
Markdown format and email it to <strong>{auth.user.email}</strong>.
</HelpText>
<Button type="submit" disabled={this.isLoading} primary>
{this.isLoading ? 'Requesting Export…' : 'Export Collection'}
</Button>
</form>
</Flex>
);
}
}
export default inject('ui', 'auth')(CollectionExport);

View File

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

View File

@@ -0,0 +1,71 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import AuthStore from 'stores/AuthStore';
import CollectionsStore from 'stores/CollectionsStore';
import UiStore from 'stores/UiStore';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
import HelpText from 'components/HelpText';
import Button from 'components/Button';
type Props = {
auth: AuthStore,
collections: CollectionsStore,
ui: UiStore,
};
@observer
class Export extends React.Component<Props> {
@observable isLoading: boolean = false;
@observable isExporting: boolean = false;
handleSubmit = async (ev: SyntheticEvent<*>) => {
ev.preventDefault();
this.isLoading = true;
const success = await this.props.collections.export();
if (success) {
this.isExporting = true;
this.props.ui.showToast('Export in progress…', 'success');
}
this.isLoading = false;
};
render() {
const { auth } = this.props;
if (!auth.user) return;
return (
<CenteredContent>
<PageTitle title="Export Data" />
<h1>Export Data</h1>
<HelpText>
Exporting your teams documents may take a little time depending on the
size of your knowledgebase. Consider exporting a single document or
collection instead.
</HelpText>
<HelpText>
Still want to export everything in your wiki? Well put together a zip
file of your collections and documents in Markdown format and email it
to <strong>{auth.user.email}</strong>.
</HelpText>
<Button
type="submit"
onClick={this.handleSubmit}
disabled={this.isLoading || this.isExporting}
primary
>
{this.isExporting
? 'Export Requested'
: this.isLoading ? 'Requesting Export…' : 'Export All Data'}
</Button>
</CenteredContent>
);
}
}
export default inject('auth', 'ui', 'collections')(Export);

View File

@@ -137,6 +137,16 @@ class CollectionsStore extends BaseStore {
}
};
@action
export = async () => {
try {
await client.post('/collections.exportAll');
return true;
} catch (err) {
throw err;
}
};
@action
add = (collection: Collection): void => {
this.data.set(collection.id, collection);