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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
55
app/scenes/CollectionExport/CollectionExport.js
Normal file
55
app/scenes/CollectionExport/CollectionExport.js
Normal 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. We’ll 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);
|
||||
3
app/scenes/CollectionExport/index.js
Normal file
3
app/scenes/CollectionExport/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import CollectionExport from './CollectionExport';
|
||||
export default CollectionExport;
|
||||
71
app/scenes/Settings/Export.js
Normal file
71
app/scenes/Settings/Export.js
Normal 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? We’ll 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);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user