From 6616276e4b3cc5addda5e990b8457b374ee1d6d2 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 19 Dec 2023 10:27:31 -0500 Subject: [PATCH] feat: Drag collection into starred section to star --- app/components/Sidebar/components/Starred.tsx | 52 ++----- .../Sidebar/components/StarredLink.tsx | 134 ++++-------------- .../Sidebar/components/useDragAndDrop.ts | 81 +++++++++++ .../components/useStarLabelAndIcon.tsx | 41 ++++++ app/hooks/usePaginatedRequest.ts | 2 +- app/models/Collection.ts | 2 +- app/stores/CollectionsStore.ts | 3 +- 7 files changed, 165 insertions(+), 150 deletions(-) create mode 100644 app/components/Sidebar/components/useDragAndDrop.ts create mode 100644 app/components/Sidebar/components/useStarLabelAndIcon.tsx diff --git a/app/components/Sidebar/components/Starred.tsx b/app/components/Sidebar/components/Starred.tsx index 8de2a2c7d..544897027 100644 --- a/app/components/Sidebar/components/Starred.tsx +++ b/app/components/Sidebar/components/Starred.tsx @@ -1,7 +1,5 @@ -import fractionalIndex from "fractional-index"; import { observer } from "mobx-react"; import * as React from "react"; -import { useDrop } from "react-dnd"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import Star from "~/models/Star"; @@ -13,50 +11,22 @@ import DropCursor from "./DropCursor"; import Header from "./Header"; import PlaceholderCollections from "./PlaceholderCollections"; import Relative from "./Relative"; -import SidebarLink, { DragObject } from "./SidebarLink"; +import SidebarLink from "./SidebarLink"; import StarredContext from "./StarredContext"; import StarredLink from "./StarredLink"; +import { useDropToCreateStar, useDropToReorderStar } from "./useDragAndDrop"; const STARRED_PAGINATION_LIMIT = 10; function Starred() { - const { documents, stars } = useStores(); + const { stars } = useStores(); const { t } = useTranslation(); const { loading, next, end, error, page } = usePaginatedRequest( - stars.fetchPage, - { - limit: STARRED_PAGINATION_LIMIT, - } + stars.fetchPage ); - - // Drop to reorder star - const [{ isOverReorder, isDraggingAnyStar }, dropToReorder] = useDrop({ - accept: "star", - drop: async (item: { star: Star }) => { - void item.star.save({ - index: fractionalIndex(null, stars.orderedData[0].index), - }); - }, - collect: (monitor) => ({ - isOverReorder: !!monitor.isOver(), - isDraggingAnyStar: monitor.getItemType() === "star", - }), - }); - - // Drop to star document - const [{ documentIsOverReorder, isDraggingAnyDocument }, dropToStar] = - useDrop({ - accept: "document", - drop: async (item: DragObject) => { - const document = documents.get(item.id); - await document?.star(fractionalIndex(null, stars.orderedData[0].index)); - }, - collect: (monitor) => ({ - documentIsOverReorder: !!monitor.isOver(), - isDraggingAnyDocument: monitor.getItemType() === "document", - }), - }); + const [reorderStarMonitor, dropToReorder] = useDropToReorderStar(); + const [createStarMonitor, dropToStarRef] = useDropToCreateStar(); React.useEffect(() => { if (error) { @@ -73,17 +43,17 @@ function Starred() {
- {isDraggingAnyStar && ( + {reorderStarMonitor.isDragging && ( )} - {isDraggingAnyDocument && ( + {createStarMonitor.isDragging && ( )} diff --git a/app/components/Sidebar/components/StarredLink.tsx b/app/components/Sidebar/components/StarredLink.tsx index 048892edc..0378a94a5 100644 --- a/app/components/Sidebar/components/StarredLink.tsx +++ b/app/components/Sidebar/components/StarredLink.tsx @@ -1,17 +1,12 @@ import fractionalIndex from "fractional-index"; import { Location } from "history"; import { observer } from "mobx-react"; -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 styled from "styled-components"; 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"; import DocumentMenu from "~/menus/DocumentMenu"; @@ -21,7 +16,13 @@ import DocumentLink from "./DocumentLink"; import DropCursor from "./DropCursor"; import Folder from "./Folder"; import Relative from "./Relative"; -import SidebarLink, { DragObject } from "./SidebarLink"; +import SidebarLink from "./SidebarLink"; +import { + useDragStar, + useDropToCreateStar, + useDropToReorderStar, +} from "./useDragAndDrop"; +import { useStarLabelAndIcon } from "./useStarLabelAndIcon"; type Props = { star: Star; @@ -34,40 +35,6 @@ function useLocationStateStarred() { return location.state?.starred; } -function useLabelAndIcon({ documentId, collectionId }: Star) { - const { collections, documents } = useStores(); - const theme = useTheme(); - - if (documentId) { - const document = documents.get(documentId); - if (document) { - return { - label: document.titleWithDefault, - icon: document.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(); @@ -99,70 +66,29 @@ function StarredLink({ star }: Props) { [] ); - const { label, icon } = useLabelAndIcon(star); - - // Draggable - const [{ isDragging }, drag, preview] = useDrag({ - type: "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: Star }) => { - const next = star?.next(); - - void item.star.save({ - index: fractionalIndex(star?.index || null, next?.index || null), - }); - }, - collect: (monitor) => ({ - isOverReorder: !!monitor.isOver(), - isDraggingAny: !!monitor.canDrop(), - }), - }); - - // Drop to star document - const [{ documentIsOverReorder, isDraggingAnyDocument }, dropToStar] = - useDrop({ - accept: "document", - drop: async (item: DragObject) => { - const next = star?.next(); - const document = documents.get(item.id); - await document?.star( - fractionalIndex(star?.index || null, next?.index || null) - ); - }, - collect: (monitor) => ({ - documentIsOverReorder: !!monitor.isOver(), - isDraggingAnyDocument: monitor.getItemType() === "document", - }), - }); + const getIndex = () => { + const next = star?.next(); + return fractionalIndex(star?.index || null, next?.index || null); + }; + const { label, icon } = useStarLabelAndIcon(star); + const [{ isDragging }, draggableRef] = useDragStar(star); + const [reorderStarMonitor, dropToReorderRef] = useDropToReorderStar(getIndex); + const [createStarMonitor, dropToStarRef] = useDropToCreateStar(getIndex); const displayChildDocuments = expanded && !isDragging; const cursor = ( <> - {isDraggingAny && ( - - )} - {isDraggingAnyDocument && ( + {reorderStarMonitor.isDragging && ( + )} + {createStarMonitor.isDragging && ( + )} @@ -174,10 +100,6 @@ function StarredLink({ star }: Props) { return null; } - const { emoji } = document; - const label = emoji - ? document.title.replace(emoji, "") - : document.titleWithDefault; const collection = document.collectionId ? collections.get(document.collectionId) : undefined; @@ -188,7 +110,7 @@ function StarredLink({ star }: Props) { return ( <> - + - + diff --git a/app/components/Sidebar/components/useDragAndDrop.ts b/app/components/Sidebar/components/useDragAndDrop.ts new file mode 100644 index 000000000..0b3385cdd --- /dev/null +++ b/app/components/Sidebar/components/useDragAndDrop.ts @@ -0,0 +1,81 @@ +import fractionalIndex from "fractional-index"; +import * as React from "react"; +import { ConnectDragSource, useDrag, useDrop } from "react-dnd"; +import { getEmptyImage } from "react-dnd-html5-backend"; +import Star from "~/models/Star"; +import useStores from "~/hooks/useStores"; +import { DragObject } from "./SidebarLink"; +import { useStarLabelAndIcon } from "./useStarLabelAndIcon"; + +/** + * Hook for shared logic that allows dragging a Starred item + * + * @param star The related Star model. + */ +export function useDragStar( + star: Star +): [{ isDragging: boolean }, ConnectDragSource] { + const { label: title, icon } = useStarLabelAndIcon(star); + const [{ isDragging }, draggableRef, preview] = useDrag({ + type: "star", + item: () => ({ icon, title, star }), + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + canDrag: () => true, + }); + + React.useEffect(() => { + preview(getEmptyImage(), { captureDraggingState: true }); + }, [preview]); + + return [{ isDragging }, draggableRef]; +} + +/** + * Hook for shared logic that allows dropping documents and collections to create a star + * + * @param getIndex A function to get the index of the current item where the star should be inserted. + */ +export function useDropToCreateStar(getIndex?: () => string) { + const { documents, stars, collections } = useStores(); + + return useDrop({ + accept: ["document", "collection"], + drop: async (item: DragObject) => { + const model = documents.get(item.id) ?? collections?.get(item.id); + await model?.star( + getIndex?.() ?? fractionalIndex(null, stars.orderedData[0].index) + ); + }, + collect: (monitor) => ({ + isOverCursor: !!monitor.isOver(), + isDragging: ["document", "collection"].includes( + String(monitor.getItemType()) + ), + }), + }); +} + +/** + * Hook for shared logic that allows dropping stars to reorder + * + * @param getIndex A function to get the index of the current item where the star should be inserted. + */ +export function useDropToReorderStar(getIndex?: () => string) { + const { stars } = useStores(); + + return useDrop({ + accept: "star", + drop: async (item: { star: Star }) => { + void item.star.save({ + index: + getIndex?.() ?? fractionalIndex(null, stars.orderedData[0].index), + }); + }, + collect: (monitor) => ({ + isOverCursor: !!monitor.isOver(), + isDragging: monitor.getItemType() === "star", + }), + }); +} diff --git a/app/components/Sidebar/components/useStarLabelAndIcon.tsx b/app/components/Sidebar/components/useStarLabelAndIcon.tsx new file mode 100644 index 000000000..1595d3022 --- /dev/null +++ b/app/components/Sidebar/components/useStarLabelAndIcon.tsx @@ -0,0 +1,41 @@ +import { StarredIcon } from "outline-icons"; +import * as React from "react"; +import { useTheme } from "styled-components"; +import Star from "~/models/Star"; +import CollectionIcon from "~/components/Icons/CollectionIcon"; +import EmojiIcon from "~/components/Icons/EmojiIcon"; +import useStores from "~/hooks/useStores"; + +export function useStarLabelAndIcon({ documentId, collectionId }: Star) { + const { collections, documents } = useStores(); + const theme = useTheme(); + + if (documentId) { + const document = documents.get(documentId); + if (document) { + return { + label: document.titleWithDefault, + icon: document.emoji ? ( + + ) : ( + + ), + }; + } + } + + if (collectionId) { + const collection = collections.get(collectionId); + if (collection) { + return { + label: collection.name, + icon: , + }; + } + } + + return { + label: "", + icon: , + }; +} diff --git a/app/hooks/usePaginatedRequest.ts b/app/hooks/usePaginatedRequest.ts index ec9e7023b..caabc9e20 100644 --- a/app/hooks/usePaginatedRequest.ts +++ b/app/hooks/usePaginatedRequest.ts @@ -32,7 +32,7 @@ const DEFAULT_LIMIT = 10; */ export default function usePaginatedRequest( requestFn: (params?: PaginationParams | undefined) => Promise, - params: PaginationParams + params: PaginationParams = {} ): RequestResponse { const [data, setData] = React.useState(); const [offset, setOffset] = React.useState(INITIAL_OFFSET); diff --git a/app/models/Collection.ts b/app/models/Collection.ts index 52d84f442..942ab39aa 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -275,7 +275,7 @@ export default class Collection extends ParanoidModel { } @action - star = async () => this.store.star(this); + star = async (index?: string) => this.store.star(this, index); @action unstar = async () => this.store.unstar(this); diff --git a/app/stores/CollectionsStore.ts b/app/stores/CollectionsStore.ts index d03477123..efb6db548 100644 --- a/app/stores/CollectionsStore.ts +++ b/app/stores/CollectionsStore.ts @@ -196,9 +196,10 @@ export default class CollectionsStore extends Store { ); } - star = async (collection: Collection) => { + star = async (collection: Collection, index?: string) => { await this.rootStore.stars.create({ collectionId: collection.id, + index, }); };