Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reordering documents in collection #1722

Merged
merged 22 commits into from
Dec 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/components/DropToImport.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ class DropToImport extends React.Component<Props> {
isDragAccept,
isDragReject,
}) => (
<DropzoneContainer {...getRootProps()} {...{ isDragActive }}>
<DropzoneContainer
{...getRootProps()}
{...{ isDragActive }}
tabIndex="-1"
>
<input {...getInputProps()} />
{this.isImporting && <LoadingIndicator />}
{this.props.children}
Expand Down
18 changes: 17 additions & 1 deletion app/components/DropdownMenu/DropdownMenuItems.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import Flex from "components/Flex";
import DropdownMenu from "./DropdownMenu";
import DropdownMenuItem from "./DropdownMenuItem";

Expand All @@ -9,18 +12,21 @@ type MenuItem =
title: React.Node,
to: string,
visible?: boolean,
selected?: boolean,
disabled?: boolean,
|}
| {|
title: React.Node,
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
visible?: boolean,
selected?: boolean,
disabled?: boolean,
|}
| {|
title: React.Node,
href: string,
visible?: boolean,
selected?: boolean,
disabled?: boolean,
|}
| {|
Expand All @@ -45,6 +51,10 @@ type Props = {|
items: MenuItem[],
|};

const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
`;

export default function DropdownMenuItems({ items }: Props): React.Node {
let filtered = items.filter((item) => item.visible !== false);

Expand All @@ -71,6 +81,7 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
to={item.to}
key={index}
disabled={item.disabled}
selected={item.selected}
>
{item.title}
</DropdownMenuItem>
Expand All @@ -83,6 +94,7 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
href={item.href}
key={index}
disabled={item.disabled}
selected={item.selected}
target="_blank"
>
{item.title}
Expand All @@ -95,6 +107,7 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
<DropdownMenuItem
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
key={index}
>
{item.title}
Expand All @@ -108,7 +121,10 @@ export default function DropdownMenuItems({ items }: Props): React.Node {
style={item.style}
label={
<DropdownMenuItem disabled={item.disabled}>
{item.title}
<Flex justify="space-between" align="center" auto>
{item.title}
<Disclosure color="currentColor" />
</Flex>
</DropdownMenuItem>
}
hover={item.hover}
Expand Down
3 changes: 2 additions & 1 deletion app/components/InputSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { Outline, LabelText } from "./Input";
const Select = styled.select`
border: 0;
flex: 1;
padding: 8px 12px;
padding: 8px 0;
margin: 0 12px;
outline: none;
background: none;
color: ${(props) => props.theme.text};
Expand Down
27 changes: 23 additions & 4 deletions app/components/Sidebar/components/CollectionLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import DropToImport from "components/DropToImport";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
Expand Down Expand Up @@ -39,26 +40,40 @@ function CollectionLink({

const { documents, policies } = useStores();
const expanded = collection.id === ui.activeCollectionId;
const manualSort = collection.sort.field === "index";

// Droppable
// Drop to re-parent
const [{ isOver, canDrop }, drop] = useDrop({
accept: "document",
drop: (item, monitor) => {
if (monitor.didDrop()) return;
if (!collection) return;
documents.move(item.id, collection.id);
},
canDrop: (item, monitor) => {
return policies.abilities(collection.id).update;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
isOver: !!monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
}),
});

// Drop to reorder
const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document",
drop: async (item, monitor) => {
if (!collection) return;
documents.move(item.id, collection.id, undefined, 0);
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
}),
});

return (
<>
<div ref={drop}>
<div ref={drop} style={{ position: "relative" }}>
<DropToImport key={collection.id} collectionId={collection.id}>
<SidebarLink
key={collection.id}
Expand Down Expand Up @@ -88,10 +103,13 @@ function CollectionLink({
}
></SidebarLink>
</DropToImport>
{expanded && manualSort && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</div>

{expanded &&
collection.documents.map((node) => (
collection.documents.map((node, index) => (
<DocumentLink
key={node.id}
node={node}
Expand All @@ -100,6 +118,7 @@ function CollectionLink({
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
depth={1.5}
index={index}
/>
))}
</>
Expand Down
152 changes: 96 additions & 56 deletions app/components/Sidebar/components/DocumentLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Collection from "models/Collection";
import Document from "models/Document";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import DropCursor from "./DropCursor";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
Expand All @@ -23,16 +24,20 @@ type Props = {|
activeDocumentRef?: (?HTMLElement) => void,
prefetchDocument: (documentId: string) => Promise<void>,
depth: number,
index: number,
parentId?: string,
|};

function DocumentLink({
node,
canUpdate,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
index,
parentId,
}: Props) {
const { documents, policies } = useStores();
const { t } = useTranslation();
Expand Down Expand Up @@ -76,6 +81,14 @@ function DocumentLink({
}
}, [showChildren]);

// when the last child document is removed,
// also close the local folder state to closed
React.useEffect(() => {
if (expanded && !hasChildDocuments) {
setExpanded(false);
}
}, [expanded, hasChildDocuments]);

const handleDisclosureClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
Expand Down Expand Up @@ -108,6 +121,7 @@ function DocumentLink({

const [menuOpen, setMenuOpen] = React.useState(false);
const isMoving = documents.movingDocumentId === node.id;
const manualSort = collection?.sort.field === "index";

// Draggable
const [{ isDragging }, drag] = useDrag({
Expand All @@ -120,77 +134,101 @@ function DocumentLink({
},
});

// Droppable
const [{ isOver, canDrop }, drop] = useDrop({
// Drop to re-parent
const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
accept: "document",
drop: async (item, monitor) => {
if (monitor.didDrop()) return;
if (!collection) return;
documents.move(item.id, collection.id, node.id);
},
canDrop: (item, monitor) =>
pathToNode && !pathToNode.includes(monitor.getItem().id),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: monitor.canDrop(),
isOverReparent: !!monitor.isOver({ shallow: true }),
canDropToReparent: monitor.canDrop(),
}),
});

// Drop to reorder
const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document",
drop: async (item, monitor) => {
if (!collection) return;
if (item.id === node.id) return;

if (expanded) {
documents.move(item.id, collection.id, node.id, 0);
return;
}

documents.move(item.id, collection.id, parentId, index + 1);
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
}),
});

return (
<>
<Draggable
key={node.id}
ref={drag}
$isDragging={isDragging}
$isMoving={isMoving}
>
<div ref={drop}>
<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 && !isDragging}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={node.title || t("Untitled")}
onSubmit={handleTitleChange}
canUpdate={canUpdate}
/>
</>
}
isActiveDrop={isOver && canDrop}
depth={depth}
exact={false}
menuOpen={menuOpen}
menu={
document && !isMoving ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
<div style={{ position: "relative" }}>
<Draggable
key={node.id}
ref={drag}
$isDragging={isDragging}
$isMoving={isMoving}
>
<div ref={dropToReparent}>
<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 && !isDragging}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={node.title || t("Untitled")}
onSubmit={handleTitleChange}
canUpdate={canUpdate}
/>
</Fade>
) : undefined
}
/>
</DropToImport>
</div>
</Draggable>

</>
}
isActiveDrop={isOverReparent && canDropToReparent}
depth={depth}
exact={false}
menuOpen={menuOpen}
menu={
document && !isMoving ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
/>
</Fade>
) : undefined
}
/>
</DropToImport>
</div>
</Draggable>
{manualSort && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</div>
{expanded && !isDragging && (
<>
{node.children.map((childNode) => (
{node.children.map((childNode, index) => (
<ObservedDocumentLink
key={childNode.id}
collection={collection}
Expand All @@ -199,6 +237,8 @@ function DocumentLink({
prefetchDocument={prefetchDocument}
depth={depth + 1}
canUpdate={canUpdate}
index={index}
parentId={node.id}
/>
))}
</>
Expand Down