diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx
index 0541841af..ba9b0a3dd 100644
--- a/app/actions/definitions/documents.tsx
+++ b/app/actions/definitions/documents.tsx
@@ -581,15 +581,11 @@ export const moveDocument = createAction({
}
stores.dialogs.openModal({
- title: t("Move {{ documentName }}", {
- documentName: document.noun,
+ title: t("Move {{ documentType }}", {
+ documentType: document.noun,
}),
- content: (
-
- ),
+ isCentered: true,
+ content: ,
});
}
},
diff --git a/app/components/DocumentExplorer.tsx b/app/components/DocumentExplorer.tsx
index ccfc6105c..c2df49ad4 100644
--- a/app/components/DocumentExplorer.tsx
+++ b/app/components/DocumentExplorer.tsx
@@ -1,5 +1,5 @@
import FuzzySearch from "fuzzy-search";
-import { includes, difference, concat, filter, flatten } from "lodash";
+import { includes, difference, concat, filter } from "lodash";
import { observer } from "mobx-react";
import { StarredIcon, DocumentIcon } from "outline-icons";
import * as React from "react";
@@ -18,11 +18,10 @@ import EmojiIcon from "~/components/Icons/EmojiIcon";
import { Outline } from "~/components/Input";
import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
-import useCollectionTrees from "~/hooks/useCollectionTrees";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
-import { flattenTree, ancestors, descendants } from "~/utils/tree";
+import { ancestors, descendants } from "~/utils/tree";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
@@ -30,14 +29,16 @@ type Props = {
/** A side-effect of item selection */
onSelect: (item: NavigationNode | null) => void;
+
+ /** Items to be shown in explorer */
+ items: NavigationNode[];
};
-function DocumentExplorer({ onSubmit, onSelect }: Props) {
+function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const isMobile = useMobile();
const { collections, documents } = useStores();
const { t } = useTranslation();
const theme = useTheme();
- const collectionTrees = useCollectionTrees();
const [searchTerm, setSearchTerm] = React.useState();
const [selectedNode, selectNode] = React.useState(
@@ -58,16 +59,11 @@ function DocumentExplorer({ onSubmit, onSelect }: Props) {
const VERTICAL_PADDING = 6;
const HORIZONTAL_PADDING = 24;
- const allNodes = React.useMemo(
- () => flatten(collectionTrees.map(flattenTree)),
- [collectionTrees]
- );
-
const searchIndex = React.useMemo(() => {
- return new FuzzySearch(allNodes, ["title"], {
+ return new FuzzySearch(items, ["title"], {
caseSensitive: false,
});
- }, [allNodes]);
+ }, [items]);
React.useEffect(() => {
if (searchTerm) {
@@ -83,12 +79,12 @@ function DocumentExplorer({ onSubmit, onSelect }: Props) {
if (searchTerm) {
results = searchIndex.search(searchTerm);
} else {
- results = allNodes.filter((r) => r.type === "collection");
+ results = items.filter((item) => item.type === "collection");
}
setInitialScrollOffset(0);
setNodes(results);
- }, [searchTerm, allNodes, searchIndex]);
+ }, [searchTerm, items, searchIndex]);
React.useEffect(() => {
onSelect(selectedNode);
@@ -148,7 +144,14 @@ function DocumentExplorer({ onSubmit, onSelect }: Props) {
return selectedNodeId === nodeId;
};
+ const hasChildren = (node: number) => {
+ return nodes[node].children.length > 0;
+ };
+
const toggleCollapse = (node: number) => {
+ if (!hasChildren(node)) {
+ return;
+ }
if (isExpanded(node)) {
collapse(node);
} else {
@@ -237,7 +240,7 @@ function DocumentExplorer({ onSubmit, onSelect }: Props) {
icon={icon}
title={title}
depth={node.depth as number}
- hasChildren={node.children.length > 0}
+ hasChildren={hasChildren(index)}
/>
);
};
diff --git a/app/components/PathToDocument.tsx b/app/components/PathToDocument.tsx
deleted file mode 100644
index d35b1d149..000000000
--- a/app/components/PathToDocument.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { observer } from "mobx-react";
-import { GoToIcon } from "outline-icons";
-import * as React from "react";
-import styled from "styled-components";
-import { DocumentPath } from "~/stores/CollectionsStore";
-import Collection from "~/models/Collection";
-import Document from "~/models/Document";
-import Flex from "~/components/Flex";
-import CollectionIcon from "~/components/Icons/CollectionIcon";
-
-type Props = {
- result: DocumentPath;
- document?: Document | null | undefined;
- collection: Collection | null | undefined;
- onSuccess?: () => void;
- style?: React.CSSProperties;
- ref?: (element: React.ElementRef<"div"> | null | undefined) => void;
-};
-
-@observer
-class PathToDocument extends React.Component {
- handleClick = async (ev: React.SyntheticEvent) => {
- ev.preventDefault();
- const { document, result, onSuccess } = this.props;
- if (!document) {
- return;
- }
-
- if (result.type === "document") {
- await document.move(result.collectionId, result.id);
- } else {
- await document.move(result.collectionId);
- }
-
- if (onSuccess) {
- onSuccess();
- }
- };
-
- render() {
- const { result, collection, document, ref, style } = this.props;
- const Component = document ? ResultWrapperLink : ResultWrapper;
- if (!result) {
- return ;
- }
-
- return (
- // @ts-expect-error ts-migrate(2604) FIXME: JSX element type 'Component' does not have any con... Remove this comment to see the full error message
-
- {collection && }
-
- {result.path
- .map((doc) => {doc.title})
- // @ts-expect-error ts-migrate(2739) FIXME: Type 'Element[]' is missing the following properti... Remove this comment to see the full error message
- .reduce((prev, curr) => [prev, , curr])}
- {document && (
-
- {" "}
- {document.title}
-
- )}
-
- );
- }
-}
-
-const DocumentTitle = styled(Flex)``;
-
-const Title = styled.span`
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-`;
-
-const StyledGoToIcon = styled(GoToIcon)`
- fill: ${(props) => props.theme.divider};
-`;
-
-const ResultWrapper = styled.div`
- display: flex;
- margin-bottom: 10px;
- user-select: none;
-
- color: ${(props) => props.theme.text};
- cursor: default;
-
- svg {
- flex-shrink: 0;
- }
-`;
-
-const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
- padding: 8px 4px;
-
- ${DocumentTitle} {
- display: none;
- }
-
- svg {
- flex-shrink: 0;
- }
-
- &:hover,
- &:active,
- &:focus {
- background: ${(props) => props.theme.listItemHoverBackground};
- outline: none;
-
- ${DocumentTitle} {
- display: flex;
- }
- }
-`;
-
-export default PathToDocument;
diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx
index 190956667..fd5b1d4d4 100644
--- a/app/scenes/Document/components/Document.tsx
+++ b/app/scenes/Document/components/Document.tsx
@@ -6,7 +6,6 @@ import * as React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import {
Prompt,
- Route,
RouteComponentProps,
StaticContext,
withRouter,
@@ -28,7 +27,6 @@ import ConnectionStatus from "~/components/ConnectionStatus";
import ErrorBoundary from "~/components/ErrorBoundary";
import Flex from "~/components/Flex";
import LoadingIndicator from "~/components/LoadingIndicator";
-import Modal from "~/components/Modal";
import PageTitle from "~/components/PageTitle";
import PlaceholderDocument from "~/components/PlaceholderDocument";
import RegisterKeyDown from "~/components/RegisterKeyDown";
@@ -39,7 +37,6 @@ import { replaceTitleVariables } from "~/utils/date";
import { emojiToUrl } from "~/utils/emoji";
import { isModKey } from "~/utils/keyboard";
import {
- documentMoveUrl,
documentHistoryUrl,
editDocumentUrl,
documentUrl,
@@ -226,15 +223,15 @@ class DocumentScene extends React.Component {
}
};
- goToMove = (ev: KeyboardEvent) => {
- if (!this.props.readOnly) {
- return;
- }
+ onMove = (ev: React.MouseEvent | KeyboardEvent) => {
ev.preventDefault();
- const { document, abilities } = this.props;
-
+ const { document, dialogs, t, abilities } = this.props;
if (abilities.move) {
- this.props.history.push(documentMoveUrl(document));
+ dialogs.openModal({
+ title: t("Move document"),
+ isCentered: true,
+ content: ,
+ });
}
};
@@ -476,7 +473,7 @@ class DocumentScene extends React.Component {
}}
/>
)}
-
+
@@ -502,21 +499,6 @@ class DocumentScene extends React.Component {
column
auto
>
- (
-
-
-
- )}
- />
void;
};
-function DocumentMove({ document, onRequestClose }: Props) {
- const [searchTerm, setSearchTerm] = useState();
- const { collections, documents } = useStores();
+function DocumentMove({ document }: Props) {
+ const { dialogs } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
+ const collectionTrees = useCollectionTrees();
+ const [selectedPath, selectPath] = React.useState(
+ null
+ );
- const searchIndex = useMemo(() => {
- const paths = collections.pathsToDocuments;
-
- // Build index
- const indexeableDocuments: DocumentPath[] = [];
-
- paths.forEach((path) => {
- const doc = documents.get(path.id);
-
- if (!doc || !doc.isTemplate) {
- indexeableDocuments.push(path);
- }
- });
-
- return new FuzzySearch(indexeableDocuments, ["title"], {
- caseSensitive: false,
- sort: true,
- });
- }, [documents, collections.pathsToDocuments]);
-
- const results = useMemo(() => {
- const onlyShowCollections = document.isTemplate;
- let results: DocumentPath[] = [];
-
- if (collections.isLoaded) {
- if (searchTerm) {
- results = searchIndex.search(searchTerm);
- } else {
- results = searchIndex.haystack;
- }
- }
-
- if (onlyShowCollections) {
- results = results.filter((result) => result.type === "collection");
- } else {
- // Exclude document if on the path to result, or the same result
- results = results.filter(
- (result) =>
- !result.path.map((doc) => doc.id).includes(document.id) &&
- last(result.path.map((doc) => doc.id)) !== document.parentDocumentId
- );
- }
-
- return results;
- }, [document, collections, searchTerm, searchIndex]);
-
- const handleSuccess = () => {
- showToast(t("Document moved"), {
- type: "info",
- });
- onRequestClose();
- };
-
- const handleFilter = (ev: React.ChangeEvent) => {
- setSearchTerm(ev.target.value);
- };
-
- const renderPathToCurrentDocument = () => {
- const result = collections.getPathForDocument(document.id);
-
- if (result) {
- return (
-
- );
- }
-
- return null;
- };
-
- const row = ({
- index,
- data,
- style,
- }: {
- index: number;
- data: DocumentPath[];
- style: React.CSSProperties;
- }) => {
- const result = data[index];
- return (
-
+ const moveOptions = React.useMemo(() => {
+ // filter out the document itself and also its parent doc if any
+ const nodes = flatten(collectionTrees.map(flattenTree)).filter(
+ (node) => node.id !== document.id && node.id !== document.parentDocumentId
);
+ if (document.isTemplate) {
+ // only show collections with children stripped off to prevent node expansion
+ return nodes
+ .filter((node) => node.type === "collection")
+ .map((node) => ({ ...node, children: [] }));
+ }
+ return nodes;
+ }, [
+ collectionTrees,
+ document.id,
+ document.parentDocumentId,
+ document.isTemplate,
+ ]);
+
+ const move = async () => {
+ if (!selectedPath) {
+ showToast(t("Select a location to move"), {
+ type: "info",
+ });
+ return;
+ }
+
+ try {
+ const { type, id: parentDocumentId } = selectedPath;
+
+ const collectionId = selectedPath.collectionId as string;
+
+ if (type === "document") {
+ await document.move(collectionId, parentDocumentId);
+ } else {
+ await document.move(collectionId);
+ }
+
+ showToast(t("Document moved"), {
+ type: "success",
+ });
+
+ dialogs.closeAllModals();
+ } catch (err) {
+ showToast(t("Couldn’t move the document, try again?"), {
+ type: "error",
+ });
+ }
};
- const data = results;
-
- if (!document || !collections.isLoaded) {
- return null;
- }
-
return (
-
-
-
- {renderPathToCurrentDocument()}
-
-
-
-
-
+
+
+
+
);
}
-const Input = styled(InputSearch)`
- ${Outline} {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- padding: 4px 0;
- }
+const FlexContainer = styled(Flex)`
+ margin-left: -24px;
+ margin-right: -24px;
+ margin-bottom: -24px;
+ outline: none;
`;
-const Results = styled.div`
- padding: 0;
- height: 40vh;
- border: 1px solid ${(props) => props.theme.inputBorder};
- border-top: 0;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
+const Footer = styled(Flex)`
+ height: 64px;
+ border-top: 1px solid ${(props) => props.theme.horizontalRule};
+ padding-left: 24px;
+ padding-right: 24px;
`;
-const Section = styled(Flex)`
- margin-bottom: 24px;
+const StyledText = styled(Text)`
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 0;
`;
export default observer(DocumentMove);
diff --git a/app/scenes/DocumentPublish.tsx b/app/scenes/DocumentPublish.tsx
index fbdfa6de8..593e80837 100644
--- a/app/scenes/DocumentPublish.tsx
+++ b/app/scenes/DocumentPublish.tsx
@@ -1,3 +1,4 @@
+import { flatten } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
@@ -8,8 +9,10 @@ import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
+import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
+import { flattenTree } from "~/utils/tree";
type Props = {
/** Document to publish */
@@ -20,10 +23,14 @@ function DocumentPublish({ document }: Props) {
const { dialogs } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
-
+ const collectionTrees = useCollectionTrees();
const [selectedPath, selectPath] = React.useState(
null
);
+ const publishOptions = React.useMemo(
+ () => flatten(collectionTrees.map(flattenTree)),
+ [collectionTrees]
+ );
const publish = async () => {
if (!selectedPath) {
@@ -60,7 +67,11 @@ function DocumentPublish({ document }: Props) {
return (
-
+