Move tree implementation out of collections store (#4763)
* refactor: attaching emoji in tree node is unnecessary * refactor: pass depth and hasChildren as separate props * refactor: move tree impl into a separate hook * refactor: separate out as DocumentExplorer for reuse * fix: separate search and node * fix: review comments * fix: tsc
This commit is contained in:
@@ -3,11 +3,12 @@ import { ArchiveIcon, GoToIcon, ShapesIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import type { NavigationNode } from "@shared/types";
|
||||
import Document from "~/models/Document";
|
||||
import Breadcrumb from "~/components/Breadcrumb";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { MenuInternalLink, NavigationNode } from "~/types";
|
||||
import { MenuInternalLink } from "~/types";
|
||||
import { collectionUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
|
||||
375
app/components/DocumentExplorer.tsx
Normal file
375
app/components/DocumentExplorer.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import FuzzySearch from "fuzzy-search";
|
||||
import { includes, difference, concat, filter, flatten } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon, DocumentIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import { FixedSizeList as List } from "react-window";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
|
||||
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
|
||||
import Flex from "~/components/Flex";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
/** Action taken upon submission of selected item, could be publish, move etc. */
|
||||
onSubmit: () => void;
|
||||
|
||||
/** A side-effect of item selection */
|
||||
onSelect: (item: NavigationNode | null) => void;
|
||||
};
|
||||
|
||||
function DocumentExplorer({ onSubmit, onSelect }: Props) {
|
||||
const isMobile = useMobile();
|
||||
const { collections, documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const collectionTrees = useCollectionTrees();
|
||||
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>();
|
||||
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
|
||||
null
|
||||
);
|
||||
const [initialScrollOffset, setInitialScrollOffset] = React.useState<number>(
|
||||
0
|
||||
);
|
||||
const [nodes, setNodes] = React.useState<NavigationNode[]>([]);
|
||||
const [activeNode, setActiveNode] = React.useState<number>(0);
|
||||
const [expandedNodes, setExpandedNodes] = React.useState<string[]>([]);
|
||||
|
||||
const inputSearchRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(
|
||||
null
|
||||
);
|
||||
const listRef = React.useRef<List<NavigationNode[]>>(null);
|
||||
|
||||
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"], {
|
||||
caseSensitive: false,
|
||||
});
|
||||
}, [allNodes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchTerm) {
|
||||
selectNode(null);
|
||||
setExpandedNodes([]);
|
||||
}
|
||||
setActiveNode(0);
|
||||
}, [searchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let results;
|
||||
|
||||
if (searchTerm) {
|
||||
results = searchIndex.search(searchTerm);
|
||||
} else {
|
||||
results = allNodes.filter((r) => r.type === "collection");
|
||||
}
|
||||
|
||||
setInitialScrollOffset(0);
|
||||
setNodes(results);
|
||||
}, [searchTerm, allNodes, searchIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
onSelect(selectedNode);
|
||||
}, [selectedNode, onSelect]);
|
||||
|
||||
const handleSearch = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(ev.target.value);
|
||||
};
|
||||
|
||||
const isExpanded = (node: number) => {
|
||||
return includes(expandedNodes, nodes[node].id);
|
||||
};
|
||||
|
||||
const calculateInitialScrollOffset = (itemCount: number) => {
|
||||
if (listRef.current) {
|
||||
const { height, itemSize } = listRef.current.props;
|
||||
const { scrollOffset } = listRef.current.state as {
|
||||
scrollOffset: number;
|
||||
};
|
||||
const itemsHeight = itemCount * itemSize;
|
||||
return itemsHeight < height ? 0 : scrollOffset;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const collapse = (node: number) => {
|
||||
const descendantIds = descendants(nodes[node]).map((des) => des.id);
|
||||
setExpandedNodes(
|
||||
difference(expandedNodes, [...descendantIds, nodes[node].id])
|
||||
);
|
||||
|
||||
// remove children
|
||||
const newNodes = filter(nodes, (node) => !includes(descendantIds, node.id));
|
||||
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
|
||||
setInitialScrollOffset(scrollOffset);
|
||||
setNodes(newNodes);
|
||||
};
|
||||
|
||||
const expand = (node: number) => {
|
||||
setExpandedNodes(concat(expandedNodes, nodes[node].id));
|
||||
|
||||
// add children
|
||||
const newNodes = nodes.slice();
|
||||
newNodes.splice(node + 1, 0, ...descendants(nodes[node], 1));
|
||||
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
|
||||
setInitialScrollOffset(scrollOffset);
|
||||
setNodes(newNodes);
|
||||
};
|
||||
|
||||
const isSelected = (node: number) => {
|
||||
if (!selectedNode) {
|
||||
return false;
|
||||
}
|
||||
const selectedNodeId = selectedNode.id;
|
||||
const nodeId = nodes[node].id;
|
||||
|
||||
return selectedNodeId === nodeId;
|
||||
};
|
||||
|
||||
const toggleCollapse = (node: number) => {
|
||||
if (isExpanded(node)) {
|
||||
collapse(node);
|
||||
} else {
|
||||
expand(node);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelect = (node: number) => {
|
||||
if (isSelected(node)) {
|
||||
selectNode(null);
|
||||
} else {
|
||||
selectNode(nodes[node]);
|
||||
}
|
||||
};
|
||||
|
||||
const ListItem = ({
|
||||
index,
|
||||
data,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
data: NavigationNode[];
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const node = data[index];
|
||||
const isCollection = node.type === "collection";
|
||||
let icon, title, path;
|
||||
|
||||
if (isCollection) {
|
||||
const col = collections.get(node.collectionId as string);
|
||||
icon = col && (
|
||||
<CollectionIcon collection={col} expanded={isExpanded(index)} />
|
||||
);
|
||||
title = node.title;
|
||||
} else {
|
||||
const doc = documents.get(node.id);
|
||||
const { strippedTitle, emoji } = parseTitle(node.title);
|
||||
title = strippedTitle;
|
||||
|
||||
if (emoji) {
|
||||
icon = <EmojiIcon emoji={emoji} />;
|
||||
} else if (doc?.isStarred) {
|
||||
icon = <StarredIcon color={theme.yellow} />;
|
||||
} else {
|
||||
icon = <DocumentIcon />;
|
||||
}
|
||||
|
||||
path = ancestors(node)
|
||||
.map((a) => parseTitle(a.title).strippedTitle)
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
return searchTerm ? (
|
||||
<DocumentExplorerSearchResult
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
path={path}
|
||||
/>
|
||||
) : (
|
||||
<DocumentExplorerNode
|
||||
style={{
|
||||
...style,
|
||||
top: (style.top as number) + VERTICAL_PADDING,
|
||||
left: (style.left as number) + HORIZONTAL_PADDING,
|
||||
width: `calc(${style.width} - ${HORIZONTAL_PADDING * 2}px)`,
|
||||
}}
|
||||
onPointerMove={() => setActiveNode(index)}
|
||||
onClick={() => toggleSelect(index)}
|
||||
onDisclosureClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
toggleCollapse(index);
|
||||
}}
|
||||
selected={isSelected(index)}
|
||||
active={activeNode === index}
|
||||
expanded={isExpanded(index)}
|
||||
icon={icon}
|
||||
title={title}
|
||||
depth={node.depth as number}
|
||||
hasChildren={node.children.length > 0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const focusSearchInput = () => {
|
||||
inputSearchRef.current?.focus();
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
return Math.min(activeNode + 1, nodes.length - 1);
|
||||
};
|
||||
|
||||
const prev = () => {
|
||||
return Math.max(activeNode - 1, 0);
|
||||
};
|
||||
|
||||
const handleKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
switch (ev.key) {
|
||||
case "ArrowDown": {
|
||||
ev.preventDefault();
|
||||
setActiveNode(next());
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
ev.preventDefault();
|
||||
if (activeNode === 0) {
|
||||
focusSearchInput();
|
||||
} else {
|
||||
setActiveNode(prev());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
if (!searchTerm && isExpanded(activeNode)) {
|
||||
toggleCollapse(activeNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
if (!searchTerm) {
|
||||
toggleCollapse(activeNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Enter": {
|
||||
if (isModKey(ev)) {
|
||||
onSubmit();
|
||||
} else {
|
||||
toggleSelect(activeNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const innerElementType = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ style, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Container tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<ListSearch
|
||||
ref={inputSearchRef}
|
||||
onChange={handleSearch}
|
||||
placeholder={`${t("Search collections & documents")}…`}
|
||||
autoFocus
|
||||
/>
|
||||
<ListContainer>
|
||||
{nodes.length ? (
|
||||
<AutoSizer>
|
||||
{({ width, height }: { width: number; height: number }) => (
|
||||
<Flex role="listbox" column>
|
||||
<List
|
||||
ref={listRef}
|
||||
key={nodes.length}
|
||||
width={width}
|
||||
height={height}
|
||||
itemData={nodes}
|
||||
itemCount={nodes.length}
|
||||
itemSize={isMobile ? 48 : 32}
|
||||
innerElementType={innerElementType}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
itemKey={(index, results) => results[index].id}
|
||||
>
|
||||
{ListItem}
|
||||
</List>
|
||||
</Flex>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
<FlexContainer>
|
||||
<Text type="secondary">{t("No results found")}.</Text>
|
||||
</FlexContainer>
|
||||
)}
|
||||
</ListContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div``;
|
||||
|
||||
const FlexContainer = styled(Flex)`
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ListSearch = styled(InputSearch)`
|
||||
${Outline} {
|
||||
border-radius: 16px;
|
||||
}
|
||||
margin-bottom: 4px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
`;
|
||||
|
||||
const ListContainer = styled.div`
|
||||
height: 65vh;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
height: 40vh;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(DocumentExplorer);
|
||||
@@ -7,49 +7,40 @@ import breakpoint from "styled-components-breakpoint";
|
||||
import Flex from "~/components/Flex";
|
||||
import Disclosure from "~/components/Sidebar/components/Disclosure";
|
||||
import Text from "~/components/Text";
|
||||
import { ancestors } from "~/utils/tree";
|
||||
|
||||
type Props = {
|
||||
location: any;
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
style: React.CSSProperties;
|
||||
isSearchResult: boolean;
|
||||
expanded: boolean;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
depth: number;
|
||||
hasChildren: boolean;
|
||||
|
||||
onDisclosureClick: (ev: React.MouseEvent) => void;
|
||||
onPointerMove: (ev: React.MouseEvent) => void;
|
||||
onClick: (ev: React.MouseEvent) => void;
|
||||
};
|
||||
|
||||
function PublishLocation({
|
||||
location,
|
||||
function DocumentExplorerNode({
|
||||
selected,
|
||||
active,
|
||||
style,
|
||||
isSearchResult,
|
||||
expanded,
|
||||
icon,
|
||||
title,
|
||||
depth,
|
||||
hasChildren,
|
||||
onDisclosureClick,
|
||||
onPointerMove,
|
||||
onClick,
|
||||
icon,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const OFFSET = 12;
|
||||
const ICON_SIZE = 24;
|
||||
|
||||
const hasChildren = location.children.length > 0;
|
||||
const isCollection = location.data.type === "collection";
|
||||
|
||||
const width = location.depth
|
||||
? location.depth * ICON_SIZE + OFFSET
|
||||
: ICON_SIZE;
|
||||
|
||||
const path = (location: any) =>
|
||||
ancestors(location)
|
||||
.map((a) => a.data.title)
|
||||
.join(" / ");
|
||||
const width = depth ? depth * ICON_SIZE + OFFSET : ICON_SIZE;
|
||||
|
||||
const ref = React.useCallback(
|
||||
(node: HTMLSpanElement | null) => {
|
||||
@@ -65,7 +56,7 @@ function PublishLocation({
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
<Node
|
||||
ref={ref}
|
||||
selected={selected}
|
||||
active={active}
|
||||
@@ -74,45 +65,29 @@ function PublishLocation({
|
||||
onPointerMove={onPointerMove}
|
||||
role="option"
|
||||
>
|
||||
{!isSearchResult && (
|
||||
<Spacer width={width}>
|
||||
{hasChildren && (
|
||||
<StyledDisclosure
|
||||
expanded={expanded}
|
||||
onClick={onDisclosureClick}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</Spacer>
|
||||
)}
|
||||
<Spacer width={width}>
|
||||
{hasChildren && (
|
||||
<StyledDisclosure
|
||||
expanded={expanded}
|
||||
onClick={onDisclosureClick}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</Spacer>
|
||||
{icon}
|
||||
<Title>{location.data.title || t("Untitled")}</Title>
|
||||
{isSearchResult && !isCollection && (
|
||||
<Path $selected={selected} size="xsmall">
|
||||
{path(location)}
|
||||
</Path>
|
||||
)}
|
||||
</Row>
|
||||
<Title>{title || t("Untitled")}</Title>
|
||||
</Node>
|
||||
);
|
||||
}
|
||||
|
||||
const Title = styled(Text)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 4px 0 4px;
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
const Path = styled(Text)<{ $selected: boolean }>`
|
||||
padding-top: 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 4px 0 8px;
|
||||
color: ${(props) =>
|
||||
props.$selected ? props.theme.white50 : props.theme.textTertiary};
|
||||
`;
|
||||
|
||||
const StyledDisclosure = styled(Disclosure)`
|
||||
position: relative;
|
||||
left: auto;
|
||||
@@ -125,7 +100,7 @@ const Spacer = styled(Flex)<{ width: number }>`
|
||||
width: ${(props) => props.width}px;
|
||||
`;
|
||||
|
||||
const Row = styled.span<{
|
||||
export const Node = styled.span<{
|
||||
active: boolean;
|
||||
selected: boolean;
|
||||
style: React.CSSProperties;
|
||||
@@ -167,4 +142,4 @@ const Row = styled.span<{
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(PublishLocation);
|
||||
export default observer(DocumentExplorerNode);
|
||||
85
app/components/DocumentExplorerSearchResult.tsx
Normal file
85
app/components/DocumentExplorerSearchResult.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import styled from "styled-components";
|
||||
import { Node as SearchResult } from "~/components/DocumentExplorerNode";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
type Props = {
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
style: React.CSSProperties;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
path?: string;
|
||||
|
||||
onPointerMove: (ev: React.MouseEvent) => void;
|
||||
onClick: (ev: React.MouseEvent) => void;
|
||||
};
|
||||
|
||||
function DocumentExplorerSearchResult({
|
||||
selected,
|
||||
active,
|
||||
style,
|
||||
icon,
|
||||
title,
|
||||
path,
|
||||
onPointerMove,
|
||||
onClick,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ref = React.useCallback(
|
||||
(node: HTMLSpanElement | null) => {
|
||||
if (active && node) {
|
||||
scrollIntoView(node, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "auto",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
},
|
||||
[active]
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchResult
|
||||
ref={ref}
|
||||
selected={selected}
|
||||
active={active}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
onPointerMove={onPointerMove}
|
||||
role="option"
|
||||
>
|
||||
{icon}
|
||||
<Flex>
|
||||
<Title>{title || t("Untitled")}</Title>
|
||||
<Path $selected={selected} size="xsmall">
|
||||
{path}
|
||||
</Path>
|
||||
</Flex>
|
||||
</SearchResult>
|
||||
);
|
||||
}
|
||||
|
||||
const Title = styled(Text)`
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
margin: 0 4px 0 4px;
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
const Path = styled(Text)<{ $selected: boolean }>`
|
||||
padding-top: 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 4px 0 8px;
|
||||
color: ${(props) =>
|
||||
props.$selected ? props.theme.white50 : props.theme.textTertiary};
|
||||
`;
|
||||
|
||||
export default observer(DocumentExplorerSearchResult);
|
||||
@@ -2,11 +2,11 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import Team from "~/models/Team";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import SearchPopover from "~/components/SearchPopover";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { NavigationNode } from "~/types";
|
||||
import history from "~/utils/history";
|
||||
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
|
||||
import TeamLogo from "../TeamLogo";
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentReparent from "~/scenes/DocumentReparent";
|
||||
@@ -17,7 +18,6 @@ import useBoolean from "~/hooks/useBoolean";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import CollectionMenu from "~/menus/CollectionMenu";
|
||||
import { NavigationNode } from "~/types";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import Relative from "./Relative";
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
import { DocumentValidation } from "@shared/validations";
|
||||
import Collection from "~/models/Collection";
|
||||
@@ -18,7 +19,6 @@ import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||
import Disclosure from "./Disclosure";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
|
||||
@@ -2,10 +2,10 @@ import { LocationDescriptor } from "history";
|
||||
import * as React from "react";
|
||||
import styled, { useTheme, css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import EventBoundary from "~/components/EventBoundary";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
import { NavigationNode } from "~/types";
|
||||
import Disclosure from "./Disclosure";
|
||||
import NavLink, { Props as NavLinkProps } from "./NavLink";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user