feat: Add reordering to starred documents (#2953)
* draft * reordering * JIT Index stars on first load * test * Remove unused code on client * small unrefactor
This commit is contained in:
@@ -73,7 +73,7 @@ function Collections() {
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
from="collections"
|
||||
position="top"
|
||||
/>
|
||||
{orderedCollections.map((collection: Collection, index: number) => (
|
||||
<CollectionLink
|
||||
|
||||
@@ -4,27 +4,26 @@ import styled from "styled-components";
|
||||
function DropCursor({
|
||||
isActiveDrop,
|
||||
innerRef,
|
||||
from,
|
||||
position,
|
||||
}: {
|
||||
isActiveDrop: boolean;
|
||||
innerRef: React.Ref<HTMLDivElement>;
|
||||
from?: string;
|
||||
position?: "top";
|
||||
}) {
|
||||
return <Cursor isOver={isActiveDrop} ref={innerRef} from={from} />;
|
||||
return <Cursor isOver={isActiveDrop} ref={innerRef} position={position} />;
|
||||
}
|
||||
|
||||
// transparent hover zone with a thin visible band vertically centered
|
||||
const Cursor = styled.div<{ isOver?: boolean; from?: string }>`
|
||||
const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>`
|
||||
opacity: ${(props) => (props.isOver ? 1 : 0)};
|
||||
transition: opacity 150ms;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
${(props) => (props.from === "collections" ? "top: 25px;" : "bottom: -7px;")}
|
||||
background: transparent;
|
||||
${(props) => (props.position === "top" ? "top: 25px;" : "bottom: -7px;")}
|
||||
|
||||
::after {
|
||||
background: ${(props) => props.theme.slateDark};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Star from "~/models/Star";
|
||||
import Flex from "~/components/Flex";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import DropCursor from "./DropCursor";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Section from "./Section";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
@@ -23,14 +26,13 @@ function Starred() {
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
|
||||
const { showToast } = useToasts();
|
||||
const { documents } = useStores();
|
||||
const { stars, documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred } = documents;
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
await fetchStarred({
|
||||
await stars.fetchPage({
|
||||
limit: STARRED_PAGINATION_LIMIT,
|
||||
offset,
|
||||
});
|
||||
@@ -42,9 +44,9 @@ function Starred() {
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}, [fetchStarred, offset, showToast, t]);
|
||||
}, [stars, offset, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
let stateInLocal;
|
||||
|
||||
try {
|
||||
@@ -60,19 +62,19 @@ function Starred() {
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
setOffset(starred.length);
|
||||
React.useEffect(() => {
|
||||
setOffset(stars.orderedData.length);
|
||||
|
||||
if (starred.length <= STARRED_PAGINATION_LIMIT) {
|
||||
if (stars.orderedData.length <= STARRED_PAGINATION_LIMIT) {
|
||||
setShow("Nothing");
|
||||
} else if (starred.length >= upperBound) {
|
||||
} else if (stars.orderedData.length >= upperBound) {
|
||||
setShow("More");
|
||||
} else if (starred.length < upperBound) {
|
||||
} else if (stars.orderedData.length < upperBound) {
|
||||
setShow("Less");
|
||||
}
|
||||
}, [starred, upperBound]);
|
||||
}, [stars.orderedData, upperBound]);
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (offset === 0) {
|
||||
fetchResults();
|
||||
}
|
||||
@@ -106,20 +108,34 @@ function Starred() {
|
||||
[expanded]
|
||||
);
|
||||
|
||||
const content = starred.slice(0, upperBound).map((document) => {
|
||||
return (
|
||||
// Drop to reorder document
|
||||
const [{ isOverReorder }, dropToReorder] = useDrop({
|
||||
accept: "star",
|
||||
drop: async (item: Star) => {
|
||||
item?.save({ index: fractionalIndex(null, stars.orderedData[0].index) });
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: !!monitor.isOver(),
|
||||
}),
|
||||
});
|
||||
|
||||
const content = stars.orderedData.slice(0, upperBound).map((star) => {
|
||||
const document = documents.get(star.documentId);
|
||||
|
||||
return document ? (
|
||||
<StarredLink
|
||||
key={document.id}
|
||||
key={star.id}
|
||||
star={star}
|
||||
documentId={document.id}
|
||||
collectionId={document.collectionId}
|
||||
to={document.url}
|
||||
title={document.title}
|
||||
depth={2}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
});
|
||||
|
||||
if (!starred.length) {
|
||||
if (!stars.orderedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -133,6 +149,11 @@ function Starred() {
|
||||
/>
|
||||
{expanded && (
|
||||
<>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{content}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
@@ -148,7 +169,7 @@ function Starred() {
|
||||
depth={2}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && !starred.length && (
|
||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { MAX_TITLE_LENGTH } from "@shared/constants";
|
||||
import Star from "~/models/Star";
|
||||
import Fade from "~/components/Fade";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import Disclosure from "./Disclosure";
|
||||
import DropCursor from "./DropCursor";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
|
||||
type Props = {
|
||||
star?: Star;
|
||||
depth: number;
|
||||
title: string;
|
||||
to: string;
|
||||
@@ -20,7 +25,14 @@ type Props = {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
function StarredLink({
|
||||
depth,
|
||||
title,
|
||||
to,
|
||||
documentId,
|
||||
collectionId,
|
||||
star,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { collections, documents, policies } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
@@ -74,9 +86,37 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
setIsEditing(isEditing);
|
||||
}, []);
|
||||
|
||||
// Draggable
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: "star",
|
||||
item: () => star,
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => {
|
||||
return depth === 2;
|
||||
},
|
||||
});
|
||||
|
||||
// Drop to reorder
|
||||
const [{ isOverReorder, isDraggingAny }, dropToReorder] = useDrop({
|
||||
accept: "star",
|
||||
drop: (item: Star) => {
|
||||
const next = star?.next();
|
||||
|
||||
item?.save({
|
||||
index: fractionalIndex(star?.index || null, next?.index || null),
|
||||
});
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: !!monitor.isOver(),
|
||||
isDraggingAny: !!monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Relative>
|
||||
<Draggable key={documentId} ref={drag} $isDragging={isDragging}>
|
||||
<SidebarLink
|
||||
depth={depth}
|
||||
to={`${to}?starred`}
|
||||
@@ -114,7 +154,10 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Relative>
|
||||
{isDraggingAny && (
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
</Draggable>
|
||||
{expanded &&
|
||||
childDocuments.map((childDocument) => (
|
||||
<ObserveredStarredLink
|
||||
@@ -130,8 +173,9 @@ function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
const Draggable = styled.div<{ $isDragging?: boolean }>`
|
||||
position: relative;
|
||||
opacity: ${(props) => (props.$isDragging ? 0.5 : 1)};
|
||||
`;
|
||||
|
||||
const ObserveredStarredLink = observer(StarredLink);
|
||||
|
||||
Reference in New Issue
Block a user