chore: Flatten left nav in preparation to refactor drag to reorder (#1689)

* flatten hierarchy
* fix drop to import positioning on collections
This commit is contained in:
Nan Yu
2020-12-01 21:59:18 -08:00
committed by GitHub
parent fe62048890
commit 474fbf07e6
5 changed files with 199 additions and 236 deletions

View File

@@ -26,7 +26,7 @@ function Sidebar({ location, children }: Props) {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
}, [ui, location]);
}, [ui, location, previousLocation]);
const content = (
<Container

View File

@@ -2,13 +2,11 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import DropToImport from "components/DropToImport";
import Flex from "components/Flex";
import DocumentLink from "./DocumentLink";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
@@ -18,7 +16,6 @@ type Props = {|
collection: Collection,
ui: UiStore,
canUpdate: boolean,
documents: DocumentsStore,
activeDocument: ?Document,
prefetchDocument: (id: string) => Promise<void>,
|};
@@ -34,61 +31,62 @@ class CollectionLink extends React.Component<Props> {
render() {
const {
collection,
documents,
activeDocument,
prefetchDocument,
canUpdate,
ui,
} = this.props;
const expanded = collection.id === ui.activeCollectionId;
return (
<DropToImport
key={collection.id}
collectionId={collection.id}
activeClassName="activeDropZone"
>
<SidebarLink
<>
<DropToImport
key={collection.id}
to={collection.url}
icon={<CollectionIcon collection={collection} expanded={expanded} />}
iconColor={collection.color}
expanded={expanded}
hideDisclosure
menuOpen={this.menuOpen}
label={
<EditableTitle
title={collection.name}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
/>
}
exact={false}
menu={
<CollectionMenu
position="right"
collection={collection}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
}
collectionId={collection.id}
activeClassName="activeDropZone"
>
<Flex column>
{collection.documents.map((node) => (
<DocumentLink
key={node.id}
node={node}
documents={documents}
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
<SidebarLink
key={collection.id}
to={collection.url}
icon={
<CollectionIcon collection={collection} expanded={expanded} />
}
iconColor={collection.color}
expanded={expanded}
menuOpen={this.menuOpen}
label={
<EditableTitle
title={collection.name}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
depth={1.5}
/>
))}
</Flex>
</SidebarLink>
</DropToImport>
}
exact={false}
menu={
<CollectionMenu
position="right"
collection={collection}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
}
></SidebarLink>
</DropToImport>
{expanded &&
collection.documents.map((node) => (
<DocumentLink
key={node.id}
node={node}
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
depth={1.5}
/>
))}
</>
);
}
}

View File

@@ -61,7 +61,6 @@ class Collections extends React.Component<Props> {
{collections.orderedData.map((collection) => (
<CollectionLink
key={collection.id}
documents={documents}
collection={collection}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}

View File

@@ -1,167 +1,174 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import Flex from "components/Flex";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
type Props = {|
node: NavigationNode,
documents: DocumentsStore,
canUpdate: boolean,
collection?: Collection,
activeDocument: ?Document,
activeDocumentRef?: (?HTMLElement) => void,
prefetchDocument: (documentId: string) => Promise<void>,
depth: number,
t: TFunction,
|};
@observer
class DocumentLink extends React.Component<Props> {
@observable menuOpen = false;
function DocumentLink({
node,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
}: Props) {
const { documents } = useStores();
const { t } = useTranslation();
componentDidMount() {
if (this.isActiveDocument() && this.hasChildDocuments()) {
this.props.documents.fetchChildDocuments(this.props.node.id);
const isActiveDocument = activeDocument && activeDocument.id === node.id;
const hasChildDocuments = !!node.children.length;
const document = documents.get(node.id);
const { fetchChildDocuments } = documents;
React.useEffect(() => {
if (isActiveDocument && hasChildDocuments) {
fetchChildDocuments(node.id);
}
}
}, [fetchChildDocuments, node, hasChildDocuments, isActiveDocument]);
componentDidUpdate(prevProps: Props) {
if (prevProps.activeDocument !== this.props.activeDocument) {
if (this.isActiveDocument() && this.hasChildDocuments()) {
this.props.documents.fetchChildDocuments(this.props.node.id);
}
}
}
handleMouseEnter = (ev: SyntheticEvent<>) => {
const { node, prefetchDocument } = this.props;
ev.stopPropagation();
ev.preventDefault();
prefetchDocument(node.id);
};
handleTitleChange = async (title: string) => {
const document = this.props.documents.get(this.props.node.id);
if (!document) return;
await this.props.documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
};
isActiveDocument = () => {
return (
this.props.activeDocument &&
this.props.activeDocument.id === this.props.node.id
);
};
hasChildDocuments = () => {
return !!this.props.node.children.length;
};
render() {
const {
node,
documents,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
t,
} = this.props;
const showChildren = !!(
const showChildren = React.useMemo(() => {
return !!(
hasChildDocuments &&
activeDocument &&
collection &&
(collection
.pathToDocument(activeDocument)
.map((entry) => entry.id)
.includes(node.id) ||
this.isActiveDocument())
isActiveDocument)
);
const document = documents.get(node.id);
const title = node.title || t("Untitled");
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
return (
<Flex
column
key={node.id}
ref={this.isActiveDocument() ? activeDocumentRef : undefined}
onMouseEnter={this.handleMouseEnter}
>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
to={{
pathname: node.url,
state: { title: node.title },
}}
expanded={showChildren ? true : undefined}
label={
const [expanded, setExpanded] = React.useState(showChildren);
React.useEffect(() => {
if (showChildren) {
setExpanded(showChildren);
}
}, [showChildren]);
const handleDisclosureClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleMouseEnter = React.useCallback(
(ev: SyntheticEvent<>) => {
prefetchDocument(node.id);
},
[prefetchDocument, node]
);
const handleTitleChange = React.useCallback(
async (title: string) => {
if (!document) return;
await documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
},
[documents, document]
);
const [menuOpen, setMenuOpen] = React.useState(false);
return (
<React.Fragment key={node.id}>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
innerRef={isActiveDocument ? activeDocumentRef : undefined}
onMouseEnter={handleMouseEnter}
to={{
pathname: node.url,
state: { title: node.title },
}}
label={
<>
{hasChildDocuments && (
<Disclosure
expanded={expanded}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={title}
onSubmit={this.handleTitleChange}
title={node.title || t("Untitled")}
onSubmit={handleTitleChange}
canUpdate={canUpdate}
/>
}
depth={depth}
exact={false}
menuOpen={this.menuOpen}
menu={
document ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
</Fade>
) : undefined
}
>
{this.hasChildDocuments() && (
<DocumentChildren column>
{node.children.map((childNode) => (
<DocumentLink
key={childNode.id}
collection={collection}
node={childNode}
documents={documents}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
depth={depth + 1}
canUpdate={canUpdate}
t={t}
/>
))}
</DocumentChildren>
)}
</SidebarLink>
</DropToImport>
</Flex>
);
}
</>
}
depth={depth}
exact={false}
menuOpen={menuOpen}
menu={
document ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
/>
</Fade>
) : undefined
}
></SidebarLink>
</DropToImport>
{expanded && (
<>
{node.children.map((childNode) => (
<ObservedDocumentLink
key={childNode.id}
collection={collection}
node={childNode}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
depth={depth + 1}
canUpdate={canUpdate}
/>
))}
</>
)}
</React.Fragment>
);
}
const DocumentChildren = styled(Flex)``;
const Disclosure = styled(CollapsedIcon)`
position: absolute;
left: -24px;
export default withTranslation()<DocumentLink>(DocumentLink);
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
const ObservedDocumentLink = observer(DocumentLink);
export default ObservedDocumentLink;

View File

@@ -1,23 +1,20 @@
// @flow
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withRouter, NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import { type Theme } from "types";
type Props = {
to?: string | Object,
href?: string | Object,
innerRef?: (?HTMLElement) => void,
onClick?: (SyntheticEvent<>) => void,
onMouseEnter?: (SyntheticEvent<>) => void,
children?: React.Node,
icon?: React.Node,
expanded?: boolean,
label?: React.Node,
menu?: React.Node,
menuOpen?: boolean,
hideDisclosure?: boolean,
iconColor?: string,
active?: boolean,
theme: Theme,
@@ -29,46 +26,25 @@ function SidebarLink({
icon,
children,
onClick,
onMouseEnter,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
innerRef,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
};
}, [depth]);
React.useEffect(() => {
if (rest.expanded !== undefined) {
setExpanded(rest.expanded);
}
}, [rest.expanded]);
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleExpand = React.useCallback(() => {
setExpanded(true);
}, []);
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: theme.text,
background: theme.sidebarItemBackground,
@@ -77,27 +53,21 @@ function SidebarLink({
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
onMouseEnter={onMouseEnter}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
ref={innerRef}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
);
}
@@ -153,10 +123,6 @@ const StyledNavLink = styled(NavLink)`
}
`;
const Wrapper = styled(Flex)`
position: relative;
`;
const Label = styled.div`
position: relative;
width: 100%;
@@ -164,11 +130,4 @@ const Label = styled.div`
line-height: 1.6;
`;
const Disclosure = styled(CollapsedIcon)`
position: absolute;
left: -24px;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default withRouter(withTheme(observer(SidebarLink)));
export default withRouter(withTheme(SidebarLink));