feat: Drag collection into starred section to star
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import fractionalIndex from "fractional-index";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useDrop } from "react-dnd";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import Star from "~/models/Star";
|
import Star from "~/models/Star";
|
||||||
@@ -13,50 +11,22 @@ import DropCursor from "./DropCursor";
|
|||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import PlaceholderCollections from "./PlaceholderCollections";
|
import PlaceholderCollections from "./PlaceholderCollections";
|
||||||
import Relative from "./Relative";
|
import Relative from "./Relative";
|
||||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
import SidebarLink from "./SidebarLink";
|
||||||
import StarredContext from "./StarredContext";
|
import StarredContext from "./StarredContext";
|
||||||
import StarredLink from "./StarredLink";
|
import StarredLink from "./StarredLink";
|
||||||
|
import { useDropToCreateStar, useDropToReorderStar } from "./useDragAndDrop";
|
||||||
|
|
||||||
const STARRED_PAGINATION_LIMIT = 10;
|
const STARRED_PAGINATION_LIMIT = 10;
|
||||||
|
|
||||||
function Starred() {
|
function Starred() {
|
||||||
const { documents, stars } = useStores();
|
const { stars } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { loading, next, end, error, page } = usePaginatedRequest<Star>(
|
const { loading, next, end, error, page } = usePaginatedRequest<Star>(
|
||||||
stars.fetchPage,
|
stars.fetchPage
|
||||||
{
|
|
||||||
limit: STARRED_PAGINATION_LIMIT,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
const [reorderStarMonitor, dropToReorder] = useDropToReorderStar();
|
||||||
// Drop to reorder star
|
const [createStarMonitor, dropToStarRef] = useDropToCreateStar();
|
||||||
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",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -73,17 +43,17 @@ function Starred() {
|
|||||||
<Flex column>
|
<Flex column>
|
||||||
<Header id="starred" title={t("Starred")}>
|
<Header id="starred" title={t("Starred")}>
|
||||||
<Relative>
|
<Relative>
|
||||||
{isDraggingAnyStar && (
|
{reorderStarMonitor.isDragging && (
|
||||||
<DropCursor
|
<DropCursor
|
||||||
isActiveDrop={isOverReorder}
|
isActiveDrop={reorderStarMonitor.isOverCursor}
|
||||||
innerRef={dropToReorder}
|
innerRef={dropToReorder}
|
||||||
position="top"
|
position="top"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isDraggingAnyDocument && (
|
{createStarMonitor.isDragging && (
|
||||||
<DropCursor
|
<DropCursor
|
||||||
isActiveDrop={documentIsOverReorder}
|
isActiveDrop={createStarMonitor.isOverCursor}
|
||||||
innerRef={dropToStar}
|
innerRef={dropToStarRef}
|
||||||
position="top"
|
position="top"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import fractionalIndex from "fractional-index";
|
import fractionalIndex from "fractional-index";
|
||||||
import { Location } from "history";
|
import { Location } from "history";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { StarredIcon } from "outline-icons";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } 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 { useLocation } from "react-router-dom";
|
||||||
import styled, { useTheme } from "styled-components";
|
import styled from "styled-components";
|
||||||
import Star from "~/models/Star";
|
import Star from "~/models/Star";
|
||||||
import Fade from "~/components/Fade";
|
import Fade from "~/components/Fade";
|
||||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
|
||||||
import EmojiIcon from "~/components/Icons/EmojiIcon";
|
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import useBoolean from "~/hooks/useBoolean";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import DocumentMenu from "~/menus/DocumentMenu";
|
import DocumentMenu from "~/menus/DocumentMenu";
|
||||||
@@ -21,7 +16,13 @@ import DocumentLink from "./DocumentLink";
|
|||||||
import DropCursor from "./DropCursor";
|
import DropCursor from "./DropCursor";
|
||||||
import Folder from "./Folder";
|
import Folder from "./Folder";
|
||||||
import Relative from "./Relative";
|
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 = {
|
type Props = {
|
||||||
star: Star;
|
star: Star;
|
||||||
@@ -34,40 +35,6 @@ function useLocationStateStarred() {
|
|||||||
return location.state?.starred;
|
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 ? (
|
|
||||||
<EmojiIcon emoji={document.emoji} />
|
|
||||||
) : (
|
|
||||||
<StarredIcon color={theme.yellow} />
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionId) {
|
|
||||||
const collection = collections.get(collectionId);
|
|
||||||
if (collection) {
|
|
||||||
return {
|
|
||||||
label: collection.name,
|
|
||||||
icon: <CollectionIcon collection={collection} />,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: "",
|
|
||||||
icon: <StarredIcon color={theme.yellow} />,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function StarredLink({ star }: Props) {
|
function StarredLink({ star }: Props) {
|
||||||
const { ui, collections, documents } = useStores();
|
const { ui, collections, documents } = useStores();
|
||||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||||
@@ -99,70 +66,29 @@ function StarredLink({ star }: Props) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { label, icon } = useLabelAndIcon(star);
|
const getIndex = () => {
|
||||||
|
const next = star?.next();
|
||||||
// Draggable
|
return fractionalIndex(star?.index || null, next?.index || null);
|
||||||
const [{ isDragging }, drag, preview] = useDrag({
|
};
|
||||||
type: "star",
|
const { label, icon } = useStarLabelAndIcon(star);
|
||||||
item: () => ({
|
const [{ isDragging }, draggableRef] = useDragStar(star);
|
||||||
star,
|
const [reorderStarMonitor, dropToReorderRef] = useDropToReorderStar(getIndex);
|
||||||
title: label,
|
const [createStarMonitor, dropToStarRef] = useDropToCreateStar(getIndex);
|
||||||
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 displayChildDocuments = expanded && !isDragging;
|
const displayChildDocuments = expanded && !isDragging;
|
||||||
|
|
||||||
const cursor = (
|
const cursor = (
|
||||||
<>
|
<>
|
||||||
{isDraggingAny && (
|
{reorderStarMonitor.isDragging && (
|
||||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
|
||||||
)}
|
|
||||||
{isDraggingAnyDocument && (
|
|
||||||
<DropCursor
|
<DropCursor
|
||||||
isActiveDrop={documentIsOverReorder}
|
isActiveDrop={reorderStarMonitor.isOverCursor}
|
||||||
innerRef={dropToStar}
|
innerRef={dropToReorderRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{createStarMonitor.isDragging && (
|
||||||
|
<DropCursor
|
||||||
|
isActiveDrop={createStarMonitor.isOverCursor}
|
||||||
|
innerRef={dropToStarRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -174,10 +100,6 @@ function StarredLink({ star }: Props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { emoji } = document;
|
|
||||||
const label = emoji
|
|
||||||
? document.title.replace(emoji, "")
|
|
||||||
: document.titleWithDefault;
|
|
||||||
const collection = document.collectionId
|
const collection = document.collectionId
|
||||||
? collections.get(document.collectionId)
|
? collections.get(document.collectionId)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -188,7 +110,7 @@ function StarredLink({ star }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Draggable key={star.id} ref={drag} $isDragging={isDragging}>
|
<Draggable key={star.id} ref={draggableRef} $isDragging={isDragging}>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
depth={0}
|
depth={0}
|
||||||
to={{
|
to={{
|
||||||
@@ -240,13 +162,13 @@ function StarredLink({ star }: Props) {
|
|||||||
if (collection) {
|
if (collection) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Draggable key={star?.id} ref={drag} $isDragging={isDragging}>
|
<Draggable key={star?.id} ref={draggableRef} $isDragging={isDragging}>
|
||||||
<CollectionLink
|
<CollectionLink
|
||||||
collection={collection}
|
collection={collection}
|
||||||
expanded={isDragging ? undefined : displayChildDocuments}
|
expanded={isDragging ? undefined : displayChildDocuments}
|
||||||
activeDocument={documents.active}
|
activeDocument={documents.active}
|
||||||
onDisclosureClick={handleDisclosureClick}
|
onDisclosureClick={handleDisclosureClick}
|
||||||
isDraggingAnyCollection={isDraggingAny}
|
isDraggingAnyCollection={reorderStarMonitor.isDragging}
|
||||||
/>
|
/>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
<Relative>
|
<Relative>
|
||||||
|
|||||||
81
app/components/Sidebar/components/useDragAndDrop.ts
Normal file
81
app/components/Sidebar/components/useDragAndDrop.ts
Normal file
@@ -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",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
41
app/components/Sidebar/components/useStarLabelAndIcon.tsx
Normal file
41
app/components/Sidebar/components/useStarLabelAndIcon.tsx
Normal file
@@ -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 ? (
|
||||||
|
<EmojiIcon emoji={document.emoji} />
|
||||||
|
) : (
|
||||||
|
<StarredIcon color={theme.yellow} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionId) {
|
||||||
|
const collection = collections.get(collectionId);
|
||||||
|
if (collection) {
|
||||||
|
return {
|
||||||
|
label: collection.name,
|
||||||
|
icon: <CollectionIcon collection={collection} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: "",
|
||||||
|
icon: <StarredIcon color={theme.yellow} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ const DEFAULT_LIMIT = 10;
|
|||||||
*/
|
*/
|
||||||
export default function usePaginatedRequest<T = unknown>(
|
export default function usePaginatedRequest<T = unknown>(
|
||||||
requestFn: (params?: PaginationParams | undefined) => Promise<T[]>,
|
requestFn: (params?: PaginationParams | undefined) => Promise<T[]>,
|
||||||
params: PaginationParams
|
params: PaginationParams = {}
|
||||||
): RequestResponse<T> {
|
): RequestResponse<T> {
|
||||||
const [data, setData] = React.useState<T[]>();
|
const [data, setData] = React.useState<T[]>();
|
||||||
const [offset, setOffset] = React.useState(INITIAL_OFFSET);
|
const [offset, setOffset] = React.useState(INITIAL_OFFSET);
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export default class Collection extends ParanoidModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
star = async () => this.store.star(this);
|
star = async (index?: string) => this.store.star(this, index);
|
||||||
|
|
||||||
@action
|
@action
|
||||||
unstar = async () => this.store.unstar(this);
|
unstar = async () => this.store.unstar(this);
|
||||||
|
|||||||
@@ -196,9 +196,10 @@ export default class CollectionsStore extends Store<Collection> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
star = async (collection: Collection) => {
|
star = async (collection: Collection, index?: string) => {
|
||||||
await this.rootStore.stars.create({
|
await this.rootStore.stars.create({
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
|
index,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user