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:
Tom Moor
2022-01-21 18:11:50 -08:00
committed by GitHub
parent 49533d7a3f
commit 79e2cad5b9
32 changed files with 931 additions and 132 deletions

View File

@@ -73,7 +73,7 @@ function Collections() {
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
from="collections"
position="top"
/>
{orderedCollections.map((collection: Collection, index: number) => (
<CollectionLink

View File

@@ -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};

View File

@@ -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>

View File

@@ -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);