perf: Reduce initial bundle size / async bundle loading (#1456)
* feat: Move to React.lazy * perf: Remove duplicate babel/runtime * fix: Run yarn-deduplicate * Further attempts to remove rich-markdown-editor from initial chunk * perf: Lazy loading of authenticated routes * perf: Move color picker to async loading fix: Display placeholder when loading rich editor * fix: Cache bust on auto reload
This commit is contained in:
@@ -6,7 +6,7 @@ type Props = {
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
export default function DelayedMount({ delay = 150, children }: Props) {
|
||||
export default function DelayedMount({ delay = 250, children }: Props) {
|
||||
const [isShowing, setShowing] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
import { lighten } from "polished";
|
||||
import * as React from "react";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import RichMarkdownEditor from "rich-markdown-editor";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import UiStore from "stores/UiStore";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import embeds from "../embeds";
|
||||
import isInternalUrl from "utils/isInternalUrl";
|
||||
import { uploadFile } from "utils/uploadFile";
|
||||
|
||||
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
|
||||
|
||||
const EMPTY_ARRAY = [];
|
||||
|
||||
type Props = {
|
||||
@@ -22,7 +24,7 @@ type Props = {
|
||||
};
|
||||
|
||||
type PropsWithRef = Props & {
|
||||
forwardedRef: React.Ref<RichMarkdownEditor>,
|
||||
forwardedRef: React.Ref<any>,
|
||||
history: RouterHistory,
|
||||
};
|
||||
|
||||
@@ -67,15 +69,17 @@ class Editor extends React.Component<PropsWithRef> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledEditor
|
||||
ref={this.props.forwardedRef}
|
||||
uploadImage={this.onUploadImage}
|
||||
onClickLink={this.onClickLink}
|
||||
onShowToast={this.onShowToast}
|
||||
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
|
||||
tooltip={EditorTooltip}
|
||||
{...this.props}
|
||||
/>
|
||||
<ErrorBoundary reloadOnChunkMissing>
|
||||
<StyledEditor
|
||||
ref={this.props.forwardedRef}
|
||||
uploadImage={this.onUploadImage}
|
||||
onClickLink={this.onClickLink}
|
||||
onShowToast={this.onShowToast}
|
||||
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
|
||||
tooltip={EditorTooltip}
|
||||
{...this.props}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import env from "env";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
reloadOnChunkMissing?: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -23,13 +24,25 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
this.error = error;
|
||||
console.error(error);
|
||||
|
||||
if (
|
||||
this.props.reloadOnChunkMissing &&
|
||||
error.message &&
|
||||
error.message.match(/chunk/)
|
||||
) {
|
||||
// If the editor bundle fails to load then reload the entire window. This
|
||||
// can happen if a deploy happens between the user loading the initial JS
|
||||
// bundle and the async-loaded editor JS bundle as the hash will change.
|
||||
window.location.reload(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.Sentry) {
|
||||
window.Sentry.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
window.location.reload(true);
|
||||
};
|
||||
|
||||
handleShowDetails = () => {
|
||||
|
||||
24
app/components/FullscreenLoading.js
Normal file
24
app/components/FullscreenLoading.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Empty from "components/Empty";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
|
||||
export default function FullscreenLoading() {
|
||||
return (
|
||||
<Fade timing={500}>
|
||||
<Centered>
|
||||
<Empty>Loading…</Empty>
|
||||
</Centered>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
|
||||
const Centered = styled(Flex)`
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import * as React from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import { fadeAndSlideIn } from "shared/styles/animations";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import HoverPreviewDocument from "components/HoverPreviewDocument";
|
||||
import isInternalUrl from "utils/isInternalUrl";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
|
||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||
import Editor from "components/Editor";
|
||||
@@ -29,12 +29,14 @@ function HoverPreviewDocument({ url, documents, children }: Props) {
|
||||
<Heading>{document.titleWithDefault}</Heading>
|
||||
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
|
||||
|
||||
<Editor
|
||||
key={document.id}
|
||||
defaultValue={document.getSummary()}
|
||||
disableEmbeds
|
||||
readOnly
|
||||
/>
|
||||
<React.Suspense fallback={<div />}>
|
||||
<Editor
|
||||
key={document.id}
|
||||
defaultValue={document.getSummary()}
|
||||
disableEmbeds
|
||||
readOnly
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,13 +22,17 @@ import {
|
||||
VehicleIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import styled from "styled-components";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText } from "components/Input";
|
||||
import NudeButton from "components/NudeButton";
|
||||
|
||||
const TwitterPicker = React.lazy(() =>
|
||||
import("react-color/lib/components/twitter/Twitter")
|
||||
);
|
||||
|
||||
export const icons = {
|
||||
collection: {
|
||||
component: CollectionIcon,
|
||||
@@ -193,14 +197,16 @@ class IconPicker extends React.Component<Props> {
|
||||
})}
|
||||
</Icons>
|
||||
<Flex onClick={preventEventBubble}>
|
||||
<ColorPicker
|
||||
color={this.props.color}
|
||||
onChange={(color) =>
|
||||
this.props.onChange(color.hex, this.props.icon)
|
||||
}
|
||||
colors={colors}
|
||||
triangle="hide"
|
||||
/>
|
||||
<React.Suspense fallback={<Loading>Loading…</Loading>}>
|
||||
<ColorPicker
|
||||
color={this.props.color}
|
||||
onChange={(color) =>
|
||||
this.props.onChange(color.hex, this.props.icon)
|
||||
}
|
||||
colors={colors}
|
||||
triangle="hide"
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Flex>
|
||||
</DropdownMenu>
|
||||
</Wrapper>
|
||||
@@ -226,6 +232,10 @@ const IconButton = styled(NudeButton)`
|
||||
height: 30px;
|
||||
`;
|
||||
|
||||
const Loading = styled(HelpText)`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const ColorPicker = styled(TwitterPicker)`
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Editor from "components/Editor";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText, Outline } from "components/Input";
|
||||
|
||||
type Props = {
|
||||
@@ -19,10 +21,6 @@ class InputRich extends React.Component<Props> {
|
||||
@observable editorComponent: React.ComponentType<any>;
|
||||
@observable focused: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.focused = false;
|
||||
};
|
||||
@@ -31,36 +29,18 @@ class InputRich extends React.Component<Props> {
|
||||
this.focused = true;
|
||||
};
|
||||
|
||||
loadEditor = async () => {
|
||||
try {
|
||||
const EditorImport = await import("./Editor");
|
||||
this.editorComponent = EditorImport.default;
|
||||
} catch (err) {
|
||||
if (err.message && err.message.match(/chunk/)) {
|
||||
// If the editor bundle fails to load then reload the entire window. This
|
||||
// can happen if a deploy happens between the user loading the initial JS
|
||||
// bundle and the async-loaded editor JS bundle as the hash will change.
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
|
||||
const Editor = this.editorComponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LabelText>{label}</LabelText>
|
||||
|
||||
<StyledOutline
|
||||
maxHeight={maxHeight}
|
||||
minHeight={minHeight}
|
||||
focused={this.focused}
|
||||
>
|
||||
{Editor ? (
|
||||
<React.Suspense fallback={<HelpText>Loading editor…</HelpText>}>
|
||||
<Editor
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
@@ -68,9 +48,7 @@ class InputRich extends React.Component<Props> {
|
||||
grow
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
"Loading…"
|
||||
)}
|
||||
</React.Suspense>
|
||||
</StyledOutline>
|
||||
</>
|
||||
);
|
||||
|
||||
121
app/routes.js
121
app/routes.js
@@ -1,121 +0,0 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route, Redirect, type Match } from "react-router-dom";
|
||||
import Archive from "scenes/Archive";
|
||||
import Collection from "scenes/Collection";
|
||||
import Dashboard from "scenes/Dashboard";
|
||||
import KeyedDocument from "scenes/Document/KeyedDocument";
|
||||
import DocumentNew from "scenes/DocumentNew";
|
||||
import Drafts from "scenes/Drafts";
|
||||
import Error404 from "scenes/Error404";
|
||||
import Login from "scenes/Login";
|
||||
import Search from "scenes/Search";
|
||||
import Settings from "scenes/Settings";
|
||||
import Details from "scenes/Settings/Details";
|
||||
import Events from "scenes/Settings/Events";
|
||||
import Export from "scenes/Settings/Export";
|
||||
import Groups from "scenes/Settings/Groups";
|
||||
import Notifications from "scenes/Settings/Notifications";
|
||||
import People from "scenes/Settings/People";
|
||||
import Security from "scenes/Settings/Security";
|
||||
import Shares from "scenes/Settings/Shares";
|
||||
import Slack from "scenes/Settings/Slack";
|
||||
import Tokens from "scenes/Settings/Tokens";
|
||||
import Zapier from "scenes/Settings/Zapier";
|
||||
import Starred from "scenes/Starred";
|
||||
import Templates from "scenes/Templates";
|
||||
import Trash from "scenes/Trash";
|
||||
|
||||
import Authenticated from "components/Authenticated";
|
||||
import Layout from "components/Layout";
|
||||
import SocketProvider from "components/SocketProvider";
|
||||
import { matchDocumentSlug as slug } from "utils/routeHelpers";
|
||||
|
||||
const NotFound = () => <Search notFound />;
|
||||
const RedirectDocument = ({ match }: { match: Match }) => (
|
||||
<Redirect
|
||||
to={
|
||||
match.params.documentSlug ? `/doc/${match.params.documentSlug}` : "/home"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default function Routes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/" component={Login} />
|
||||
<Route exact path="/create" component={Login} />
|
||||
<Route exact path="/share/:shareId" component={KeyedDocument} />
|
||||
<Authenticated>
|
||||
<SocketProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Dashboard} />
|
||||
<Route path="/home" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/details" component={Details} />
|
||||
<Route exact path="/settings/security" component={Security} />
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/people/:filter" component={People} />
|
||||
<Route exact path="/settings/groups" component={Groups} />
|
||||
<Route exact path="/settings/shares" component={Shares} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
<Route exact path="/settings/events" component={Events} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/notifications"
|
||||
component={Notifications}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/zapier"
|
||||
component={Zapier}
|
||||
/>
|
||||
<Route exact path="/settings/export" component={Export} />
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id/new"
|
||||
component={DocumentNew}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id/:tab"
|
||||
component={Collection}
|
||||
/>
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={KeyedDocument}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/edit`}
|
||||
component={KeyedDocument}
|
||||
/>
|
||||
<Route path={`/doc/${slug}`} component={KeyedDocument} />
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:term" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</SocketProvider>
|
||||
</Authenticated>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
76
app/routes/authenticated.js
Normal file
76
app/routes/authenticated.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route, Redirect, type Match } from "react-router-dom";
|
||||
import Archive from "scenes/Archive";
|
||||
import Collection from "scenes/Collection";
|
||||
import Dashboard from "scenes/Dashboard";
|
||||
import KeyedDocument from "scenes/Document/KeyedDocument";
|
||||
import DocumentNew from "scenes/DocumentNew";
|
||||
import Drafts from "scenes/Drafts";
|
||||
import Error404 from "scenes/Error404";
|
||||
import Search from "scenes/Search";
|
||||
import Starred from "scenes/Starred";
|
||||
import Templates from "scenes/Templates";
|
||||
import Trash from "scenes/Trash";
|
||||
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Layout from "components/Layout";
|
||||
import LoadingPlaceholder from "components/LoadingPlaceholder";
|
||||
import SocketProvider from "components/SocketProvider";
|
||||
import { matchDocumentSlug as slug } from "utils/routeHelpers";
|
||||
|
||||
const SettingsRoutes = React.lazy(() => import("./settings"));
|
||||
|
||||
const NotFound = () => <Search notFound />;
|
||||
const RedirectDocument = ({ match }: { match: Match }) => (
|
||||
<Redirect
|
||||
to={
|
||||
match.params.documentSlug ? `/doc/${match.params.documentSlug}` : "/home"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default function AuthenticatedRoutes() {
|
||||
return (
|
||||
<SocketProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Dashboard} />
|
||||
<Route path="/home" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Route exact path="/collections/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collections/:id/:tab" component={Collection} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={KeyedDocument}
|
||||
/>
|
||||
<Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} />
|
||||
<Route path={`/doc/${slug}`} component={KeyedDocument} />
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:term" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<CenteredContent>
|
||||
<LoadingPlaceholder />
|
||||
</CenteredContent>
|
||||
}
|
||||
>
|
||||
<SettingsRoutes />
|
||||
</React.Suspense>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</SocketProvider>
|
||||
);
|
||||
}
|
||||
31
app/routes/index.js
Normal file
31
app/routes/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import DelayedMount from "components/DelayedMount";
|
||||
import FullscreenLoading from "components/FullscreenLoading";
|
||||
|
||||
const Authenticated = React.lazy(() => import("components/Authenticated"));
|
||||
const AuthenticatedRoutes = React.lazy(() => import("./authenticated"));
|
||||
const KeyedDocument = React.lazy(() => import("scenes/Document/KeyedDocument"));
|
||||
const Login = React.lazy(() => import("scenes/Login"));
|
||||
|
||||
export default function Routes() {
|
||||
return (
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<DelayedMount delay={2000}>
|
||||
<FullscreenLoading />
|
||||
</DelayedMount>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Login} />
|
||||
<Route exact path="/create" component={Login} />
|
||||
<Route exact path="/share/:shareId" component={KeyedDocument} />
|
||||
<Authenticated>
|
||||
<AuthenticatedRoutes />
|
||||
</Authenticated>
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
35
app/routes/settings.js
Normal file
35
app/routes/settings.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import Settings from "scenes/Settings";
|
||||
import Details from "scenes/Settings/Details";
|
||||
import Events from "scenes/Settings/Events";
|
||||
import Export from "scenes/Settings/Export";
|
||||
import Groups from "scenes/Settings/Groups";
|
||||
import Notifications from "scenes/Settings/Notifications";
|
||||
import People from "scenes/Settings/People";
|
||||
import Security from "scenes/Settings/Security";
|
||||
import Shares from "scenes/Settings/Shares";
|
||||
import Slack from "scenes/Settings/Slack";
|
||||
import Tokens from "scenes/Settings/Tokens";
|
||||
import Zapier from "scenes/Settings/Zapier";
|
||||
|
||||
export default function SettingsRoutes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/details" component={Details} />
|
||||
<Route exact path="/settings/security" component={Security} />
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/people/:filter" component={People} />
|
||||
<Route exact path="/settings/groups" component={Groups} />
|
||||
<Route exact path="/settings/shares" component={Shares} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
<Route exact path="/settings/events" component={Events} />
|
||||
<Route exact path="/settings/notifications" component={Notifications} />
|
||||
<Route exact path="/settings/integrations/slack" component={Slack} />
|
||||
<Route exact path="/settings/integrations/zapier" component={Zapier} />
|
||||
<Route exact path="/settings/export" component={Export} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { observer, inject } from "mobx-react";
|
||||
import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
||||
import RichMarkdownEditor from "rich-markdown-editor";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
@@ -22,6 +21,7 @@ import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import DocumentList from "components/DocumentList";
|
||||
import Editor from "components/Editor";
|
||||
import Flex from "components/Flex";
|
||||
import Heading from "components/Heading";
|
||||
import HelpText from "components/HelpText";
|
||||
@@ -218,13 +218,14 @@ class CollectionScene extends React.Component<Props> {
|
||||
</Heading>
|
||||
|
||||
{collection.description && (
|
||||
<RichMarkdownEditor
|
||||
id={collection.id}
|
||||
key={collection.description}
|
||||
defaultValue={collection.description}
|
||||
theme={theme}
|
||||
readOnly
|
||||
/>
|
||||
<React.Suspense fallback={<p>Loading…</p>}>
|
||||
<Editor
|
||||
id={collection.id}
|
||||
key={collection.description}
|
||||
defaultValue={collection.description}
|
||||
readOnly
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
|
||||
{hasPinnedDocuments && (
|
||||
|
||||
@@ -128,7 +128,7 @@ class DataLoader extends React.Component<Props> {
|
||||
// If we're attempting to update an archived, deleted, or otherwise
|
||||
// uneditable document then forward to the canonical read url.
|
||||
if (!can.update && this.isEditing) {
|
||||
this.props.history.push(this.document.url);
|
||||
this.props.history.push(document.url);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ import Time from "components/Time";
|
||||
import Container from "./Container";
|
||||
import Contents from "./Contents";
|
||||
import DocumentMove from "./DocumentMove";
|
||||
import Editor from "./Editor";
|
||||
import Header from "./Header";
|
||||
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
|
||||
import Loading from "./Loading";
|
||||
import MarkAsViewed from "./MarkAsViewed";
|
||||
import References from "./References";
|
||||
import { type LocationWithState } from "types";
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
documentUrl,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
let EditorImport;
|
||||
const AUTOSAVE_DELAY = 3000;
|
||||
const IS_DIRTY_DELAY = 500;
|
||||
const DISCARD_CHANGES = `
|
||||
@@ -69,7 +68,6 @@ type Props = {
|
||||
@observer
|
||||
class DocumentScene extends React.Component<Props> {
|
||||
@observable editor: ?any;
|
||||
@observable editorComponent = EditorImport;
|
||||
@observable isUploading: boolean = false;
|
||||
@observable isSaving: boolean = false;
|
||||
@observable isPublishing: boolean = false;
|
||||
@@ -84,7 +82,6 @@ class DocumentScene extends React.Component<Props> {
|
||||
super();
|
||||
this.title = props.document.title;
|
||||
this.lastRevision = props.document.revision;
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -197,25 +194,6 @@ class DocumentScene extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
loadEditor = async () => {
|
||||
if (this.editorComponent) return;
|
||||
|
||||
try {
|
||||
const EditorImport = await import("./Editor");
|
||||
this.editorComponent = EditorImport.default;
|
||||
} catch (err) {
|
||||
if (err.message && err.message.match(/chunk/)) {
|
||||
// If the editor bundle fails to load then reload the entire window. This
|
||||
// can happen if a deploy happens between the user loading the initial JS
|
||||
// bundle and the async-loaded editor JS bundle as the hash will change.
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
handleCloseMoveModal = () => (this.moveModalOpen = false);
|
||||
handleOpenMoveModal = () => (this.moveModalOpen = true);
|
||||
|
||||
@@ -338,20 +316,14 @@ class DocumentScene extends React.Component<Props> {
|
||||
document,
|
||||
revision,
|
||||
readOnly,
|
||||
location,
|
||||
abilities,
|
||||
auth,
|
||||
ui,
|
||||
match,
|
||||
} = this.props;
|
||||
const team = auth.team;
|
||||
const Editor = this.editorComponent;
|
||||
const isShare = !!match.params.shareId;
|
||||
|
||||
if (!Editor) {
|
||||
return <Loading location={location} />;
|
||||
}
|
||||
|
||||
const value = revision ? revision.text : document.text;
|
||||
const injectTemplate = document.injectTemplate;
|
||||
const disableEmbeds =
|
||||
|
||||
@@ -3,7 +3,6 @@ import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Textarea from "react-autosize-textarea";
|
||||
import RichMarkdownEditor from "rich-markdown-editor";
|
||||
import styled from "styled-components";
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import Document from "models/Document";
|
||||
@@ -12,6 +11,7 @@ import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||
import Editor from "components/Editor";
|
||||
import Flex from "components/Flex";
|
||||
import HoverPreview from "components/HoverPreview";
|
||||
import LoadingPlaceholder from "components/LoadingPlaceholder";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
@observer
|
||||
class DocumentEditor extends React.Component<Props> {
|
||||
@observable activeLinkEvent: ?MouseEvent;
|
||||
editor = React.createRef<RichMarkdownEditor>();
|
||||
editor = React.createRef<any>();
|
||||
|
||||
focusAtStart = () => {
|
||||
if (this.editor.current) {
|
||||
@@ -78,39 +78,43 @@ class DocumentEditor extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Flex auto column>
|
||||
<Title
|
||||
type="text"
|
||||
onChange={onChangeTitle}
|
||||
onKeyDown={this.handleTitleKeyDown}
|
||||
placeholder={document.placeholder}
|
||||
value={!title && readOnly ? document.titleWithDefault : title}
|
||||
style={startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined}
|
||||
readOnly={readOnly}
|
||||
autoFocus={!title}
|
||||
maxLength={100}
|
||||
/>
|
||||
<DocumentMetaWithViews
|
||||
isDraft={isDraft}
|
||||
document={document}
|
||||
to={documentHistoryUrl(document)}
|
||||
/>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
autoFocus={title && !this.props.defaultValue}
|
||||
placeholder="…the rest is up to you"
|
||||
onHoverLink={this.handleLinkActive}
|
||||
scrollTo={window.location.hash}
|
||||
grow
|
||||
{...this.props}
|
||||
/>
|
||||
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
|
||||
{this.activeLinkEvent && !isShare && readOnly && (
|
||||
<HoverPreview
|
||||
node={this.activeLinkEvent.target}
|
||||
event={this.activeLinkEvent}
|
||||
onClose={this.handleLinkInactive}
|
||||
<React.Suspense fallback={<LoadingPlaceholder />}>
|
||||
<Title
|
||||
type="text"
|
||||
onChange={onChangeTitle}
|
||||
onKeyDown={this.handleTitleKeyDown}
|
||||
placeholder={document.placeholder}
|
||||
value={!title && readOnly ? document.titleWithDefault : title}
|
||||
style={
|
||||
startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined
|
||||
}
|
||||
readOnly={readOnly}
|
||||
autoFocus={!title}
|
||||
maxLength={100}
|
||||
/>
|
||||
)}
|
||||
<DocumentMetaWithViews
|
||||
isDraft={isDraft}
|
||||
document={document}
|
||||
to={documentHistoryUrl(document)}
|
||||
/>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
autoFocus={title && !this.props.defaultValue}
|
||||
placeholder="…the rest is up to you"
|
||||
onHoverLink={this.handleLinkActive}
|
||||
scrollTo={window.location.hash}
|
||||
grow
|
||||
{...this.props}
|
||||
/>
|
||||
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
|
||||
{this.activeLinkEvent && !isShare && readOnly && (
|
||||
<HoverPreview
|
||||
node={this.activeLinkEvent.target}
|
||||
event={this.activeLinkEvent}
|
||||
onClose={this.handleLinkInactive}
|
||||
/>
|
||||
)}
|
||||
</React.Suspense>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import { map, trim } from "lodash";
|
||||
import pkg from "rich-markdown-editor/package.json";
|
||||
import stores from "stores";
|
||||
import download from "./download";
|
||||
import {
|
||||
@@ -57,7 +56,7 @@ class ApiClient {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"cache-control": "no-cache",
|
||||
"x-editor-version": pkg.version,
|
||||
"x-editor-version": EDITOR_VERSION,
|
||||
pragma: "no-cache",
|
||||
});
|
||||
if (stores.auth.authenticated) {
|
||||
|
||||
Reference in New Issue
Block a user