diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx
index 05c9bcc16..887b079a8 100644
--- a/app/components/Sidebar/App.tsx
+++ b/app/components/Sidebar/App.tsx
@@ -25,6 +25,7 @@ import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import ArchiveLink from "./components/ArchiveLink";
import Collections from "./components/Collections";
+import DragPlaceholder from "./components/DragPlaceholder";
import HeaderButton, { HeaderButtonProps } from "./components/HeaderButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
@@ -61,6 +62,8 @@ function AppSidebar() {
{dndArea && (
+
+
{(props: HeaderButtonProps) => (
- {isDraggingAnyDocument && can.update && (
+ {isDraggingAnyDocument && can.update && manualSort && (
({
...node,
@@ -149,12 +151,13 @@ function InnerDocumentLink(
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
- canDrag: () =>
- policies.abilities(node.id).move ||
- policies.abilities(node.id).archive ||
- policies.abilities(node.id).delete,
+ canDrag: () => can.move || can.archive || can.delete,
});
+ React.useEffect(() => {
+ preview(getEmptyImage(), { captureDraggingState: true });
+ }, [preview]);
+
const hoverExpanding = React.useRef>();
// We set a timeout when the user first starts hovering over the document link,
@@ -179,10 +182,11 @@ function InnerDocumentLink(
await documents.move(item.id, collection.id, node.id);
setExpanded(true);
},
- canDrop: (_item, monitor) =>
+ canDrop: (item, monitor) =>
!isDraft &&
!!pathToNode &&
- !pathToNode.includes(monitor.getItem().id),
+ !pathToNode.includes(monitor.getItem().id) &&
+ item.id !== node.id,
hover: (_item, monitor) => {
// Enables expansion of document children when hovering over the document
// for more than half a second.
@@ -283,7 +287,6 @@ function InnerDocumentLink(
(activeDocument?.id === node.id ? activeDocument.title : node.title) ||
t("Untitled");
- const can = policies.abilities(node.id);
const isExpanded = expanded && !isDragging;
const hasChildren = nodeChildren.length > 0;
@@ -376,12 +379,8 @@ function InnerDocumentLink(
- {isDraggingAnyDocument && (
-
+ {isDraggingAnyDocument && manualSort && (
+
)}
@@ -404,7 +403,8 @@ function InnerDocumentLink(
}
const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>`
- opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
+ transition: opacity 250ms ease;
+ opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.1 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
`;
diff --git a/app/components/Sidebar/components/DragPlaceholder.tsx b/app/components/Sidebar/components/DragPlaceholder.tsx
new file mode 100644
index 000000000..289997b20
--- /dev/null
+++ b/app/components/Sidebar/components/DragPlaceholder.tsx
@@ -0,0 +1,81 @@
+import * as React from "react";
+import { useDragLayer, XYCoord } from "react-dnd";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+import useStores from "~/hooks/useStores";
+import SidebarLink from "./SidebarLink";
+
+const layerStyles: React.CSSProperties = {
+ position: "fixed",
+ pointerEvents: "none",
+ zIndex: 100,
+ left: 0,
+ top: 0,
+ width: "100%",
+ height: "100%",
+};
+
+function getItemStyles(
+ initialOffset: XYCoord | null,
+ currentOffset: XYCoord | null,
+ sidebarWidth: number
+) {
+ if (!initialOffset || !currentOffset) {
+ return {
+ display: "none",
+ };
+ }
+ const { y } = currentOffset;
+ const x = Math.max(
+ initialOffset.x,
+ Math.min(initialOffset.x + sidebarWidth / 4, currentOffset.x)
+ );
+
+ const transform = `translate(${x}px, ${y}px)`;
+ return {
+ width: sidebarWidth - 24,
+ transform,
+ WebkitTransform: transform,
+ };
+}
+
+const DragPlaceholder = () => {
+ const { t } = useTranslation();
+ const { ui } = useStores();
+
+ const { isDragging, item, initialOffset, currentOffset } = useDragLayer(
+ (monitor) => ({
+ item: monitor.getItem(),
+ itemType: monitor.getItemType(),
+ initialOffset: monitor.getInitialSourceClientOffset(),
+ currentOffset: monitor.getSourceClientOffset(),
+ isDragging: monitor.isDragging(),
+ })
+ );
+
+ if (!isDragging || !currentOffset) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+const GhostLink = styled(SidebarLink)`
+ transition: box-shadow 250ms ease-in-out;
+ box-shadow: rgb(0 0 0 / 30%) 0px 4px 15px;
+ opacity: 0.95;
+`;
+
+export default DragPlaceholder;
diff --git a/app/components/Sidebar/components/DraggableCollectionLink.tsx b/app/components/Sidebar/components/DraggableCollectionLink.tsx
index 00c1fd47e..54d1eb9aa 100644
--- a/app/components/Sidebar/components/DraggableCollectionLink.tsx
+++ b/app/components/Sidebar/components/DraggableCollectionLink.tsx
@@ -2,10 +2,12 @@ import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import * as React from "react";
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
+import { getEmptyImage } from "react-dnd-html5-backend";
import { useLocation } from "react-router-dom";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
+import CollectionIcon from "~/components/Icons/CollectionIcon";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import CollectionLink from "./CollectionLink";
@@ -62,26 +64,28 @@ function DraggableCollectionLink({
},
collect: (monitor: DropTargetMonitor) => ({
isCollectionDropping: monitor.isOver(),
- isDraggingAnyCollection: monitor.getItemType() === "collection",
+ isDraggingAnyCollection: monitor.canDrop(),
}),
});
// Drag to reorder collection
- const [{ isCollectionDragging }, dragToReorderCollection] = useDrag({
+ const [{ isDragging }, dragToReorderCollection, preview] = useDrag({
type: "collection",
- item: () => {
- return {
- id: collection.id,
- };
- },
- collect: (monitor) => ({
- isCollectionDragging: monitor.isDragging(),
+ item: () => ({
+ id: collection.id,
+ title: collection.name,
+ icon: ,
}),
- canDrag: () => {
- return can.move;
- },
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ canDrag: () => can.move,
});
+ React.useEffect(() => {
+ preview(getEmptyImage(), { captureDraggingState: false });
+ }, [preview]);
+
// If the current collection is active and relevant to the sidebar section we
// are in then expand it automatically
React.useEffect(() => {
@@ -95,14 +99,14 @@ function DraggableCollectionLink({
setExpanded((e) => !e);
}, []);
- const displayChildDocuments = expanded && !isCollectionDragging;
+ const displayChildDocuments = expanded && !isDragging;
return (
<>
`
- opacity: ${(props) => (props.$isDragging ? 0.5 : 1)};
+ transition: opacity 250ms ease;
+ opacity: ${(props) => (props.$isDragging ? 0.1 : 1)};
pointer-events: ${(props) => (props.$isDragging ? "none" : "auto")};
`;
diff --git a/app/components/Sidebar/components/DropCursor.tsx b/app/components/Sidebar/components/DropCursor.tsx
index d8bab427a..8b04f1fcf 100644
--- a/app/components/Sidebar/components/DropCursor.tsx
+++ b/app/components/Sidebar/components/DropCursor.tsx
@@ -2,27 +2,18 @@ import * as React from "react";
import styled from "styled-components";
type Props = {
- disabled?: boolean;
isActiveDrop: boolean;
innerRef: React.Ref;
position?: "top";
};
-function DropCursor({ isActiveDrop, innerRef, position, disabled }: Props) {
- return (
-
- );
+function DropCursor({ isActiveDrop, innerRef, position }: Props) {
+ return ;
}
// transparent hover zone with a thin visible band vertically centered
const Cursor = styled.div<{
isOver?: boolean;
- disabled?: boolean;
position?: "top";
}>`
opacity: ${(props) => (props.isOver ? 1 : 0)};
@@ -36,10 +27,7 @@ const Cursor = styled.div<{
${(props) => (props.position === "top" ? "top: -7px;" : "bottom: -7px;")}
::after {
- background: ${(props) =>
- props.disabled
- ? props.theme.sidebarActiveBackground
- : props.theme.slateDark};
+ background: ${(props) => props.theme.slateDark};
position: absolute;
top: 6px;
content: "";
diff --git a/app/components/Sidebar/components/Starred.tsx b/app/components/Sidebar/components/Starred.tsx
index 4ee247968..da4d79d02 100644
--- a/app/components/Sidebar/components/Starred.tsx
+++ b/app/components/Sidebar/components/Starred.tsx
@@ -55,8 +55,10 @@ function Starred() {
// Drop to reorder document
const [{ isOverReorder, isDraggingAnyStar }, dropToReorder] = useDrop({
accept: "star",
- drop: async (item: Star) => {
- item?.save({ index: fractionalIndex(null, stars.orderedData[0].index) });
+ drop: async (item: { star: Star }) => {
+ item.star.save({
+ index: fractionalIndex(null, stars.orderedData[0].index),
+ });
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
diff --git a/app/components/Sidebar/components/StarredLink.tsx b/app/components/Sidebar/components/StarredLink.tsx
index 368441c91..30560b34d 100644
--- a/app/components/Sidebar/components/StarredLink.tsx
+++ b/app/components/Sidebar/components/StarredLink.tsx
@@ -5,11 +5,13 @@ import { StarredIcon } from "outline-icons";
import * as React from "react";
import { useEffect, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
+import { getEmptyImage } from "react-dnd-html5-backend";
import { useLocation } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import parseTitle from "@shared/utils/parseTitle";
import Star from "~/models/Star";
import Fade from "~/components/Fade";
+import CollectionIcon from "~/components/Icons/CollectionIcon";
import EmojiIcon from "~/components/Icons/EmojiIcon";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
@@ -33,8 +35,45 @@ function useLocationStateStarred() {
return location.state?.starred;
}
-function StarredLink({ star }: Props) {
+function useLabelAndIcon({ documentId, collectionId }: Star) {
+ const { collections, documents } = useStores();
const theme = useTheme();
+
+ if (documentId) {
+ const document = documents.get(documentId);
+ if (document) {
+ const { emoji } = parseTitle(document?.title);
+
+ return {
+ label: emoji
+ ? document.title.replace(emoji, "")
+ : document.titleWithDefault,
+ icon: emoji ? (
+
+ ) : (
+
+ ),
+ };
+ }
+ }
+
+ if (collectionId) {
+ const collection = collections.get(collectionId);
+ if (collection) {
+ return {
+ label: collection.name,
+ icon: ,
+ };
+ }
+ }
+
+ return {
+ label: "",
+ icon: ,
+ };
+}
+
+function StarredLink({ star }: Props) {
const { ui, collections, documents } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId, collectionId } = star;
@@ -69,23 +108,33 @@ function StarredLink({ star }: Props) {
[]
);
+ const { label, icon } = useLabelAndIcon(star);
+
// Draggable
- const [{ isDragging }, drag] = useDrag({
+ const [{ isDragging }, drag, preview] = useDrag({
type: "star",
- item: () => star,
+ item: () => ({
+ star,
+ title: label,
+ icon,
+ }),
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: () => true,
});
+ React.useEffect(() => {
+ preview(getEmptyImage(), { captureDraggingState: true });
+ }, [preview]);
+
// Drop to reorder
const [{ isOverReorder, isDraggingAny }, dropToReorder] = useDrop({
accept: "star",
- drop: (item: Star) => {
+ drop: (item: { star: Star }) => {
const next = star?.next();
- item?.save({
+ item.star.save({
index: fractionalIndex(star?.index || null, next?.index || null),
});
},
@@ -104,10 +153,6 @@ function StarredLink({ star }: Props) {
}
const collection = collections.get(document.collectionId);
- const { emoji } = parseTitle(document.title);
- const label = emoji
- ? document.title.replace(emoji, "")
- : document.titleWithDefault;
const childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];
@@ -124,13 +169,7 @@ function StarredLink({ star }: Props) {
}}
expanded={hasChildDocuments && !isDragging ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
- icon={
- emoji ? (
-
- ) : (
-
- )
- }
+ icon={icon}
isActive={(match, location: Location<{ starred?: boolean }>) =>
!!match && location.state?.starred === true
}
@@ -202,7 +241,8 @@ function StarredLink({ star }: Props) {
const Draggable = styled.div<{ $isDragging?: boolean }>`
position: relative;
- opacity: ${(props) => (props.$isDragging ? 0.5 : 1)};
+ transition: opacity 250ms ease;
+ opacity: ${(props) => (props.$isDragging ? 0.1 : 1)};
`;
export default observer(StarredLink);