feat: Add ability to star collection (#3327)

* Migrations, models, commands

* ui

* Move starred hint to location state

* lint

* tsc

* refactor

* Add collection empty state in expanded sidebar

* Add empty placeholder within starred collections

* Drag and drop improves, Relative refactor

* fix: Starring untitled draft leaves empty space

* fix: Creating draft in starred collection shouldnt open main

* fix: Dupe drop cursor

* Final fixes

* fix: Canonical redirect replaces starred location state

* fix: Don't show reorder cursor at the top of collection with no permission to edit when dragging
This commit is contained in:
Tom Moor
2022-04-03 18:51:01 -07:00
committed by GitHub
parent 3de06b8005
commit 84d6bf8ddf
36 changed files with 988 additions and 635 deletions

View File

@@ -1,9 +1,11 @@
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 { useLocation } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import parseTitle from "@shared/utils/parseTitle";
import Star from "~/models/Star";
@@ -12,46 +14,51 @@ import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import Folder from "./Folder";
import Relative from "./Relative";
import SidebarLink from "./SidebarLink";
type Props = {
star?: Star;
depth: number;
title: string;
to: string;
documentId: string;
collectionId: string;
star: Star;
};
function StarredLink({
depth,
to,
documentId,
title,
collectionId,
star,
}: Props) {
function useLocationStateStarred() {
const location = useLocation<{
starred?: boolean;
}>();
return location.state?.starred;
}
function StarredLink({ star }: Props) {
const theme = useTheme();
const { collections, documents } = useStores();
const collection = collections.get(collectionId);
const document = documents.get(documentId);
const [expanded, setExpanded] = useState(false);
const { ui, collections, documents } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];
const hasChildDocuments = childDocuments.length > 0;
const { documentId, collectionId } = star;
const collection = collections.get(collectionId);
const locationStateStarred = useLocationStateStarred();
const [expanded, setExpanded] = useState(
star.collectionId === ui.activeCollectionId && !!locationStateStarred
);
React.useEffect(() => {
if (star.collectionId === ui.activeCollectionId && locationStateStarred) {
setExpanded(true);
}
}, [star.collectionId, ui.activeCollectionId, locationStateStarred]);
useEffect(() => {
async function load() {
if (!document) {
if (documentId) {
await documents.fetch(documentId);
}
}
load();
}, [collection, collectionId, collections, document, documentId, documents]);
}, [documentId, documents]);
const handleDisclosureClick = React.useCallback(
(ev: React.MouseEvent<HTMLButtonElement>) => {
@@ -69,9 +76,7 @@ function StarredLink({
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: () => {
return depth === 0;
},
canDrag: () => true,
});
// Drop to reorder
@@ -90,61 +95,109 @@ function StarredLink({
}),
});
const { emoji } = parseTitle(title);
const label = emoji ? title.replace(emoji, "") : title;
const displayChildDocuments = expanded && !isDragging;
return (
<>
<Draggable key={documentId} ref={drag} $isDragging={isDragging}>
<SidebarLink
depth={depth}
expanded={hasChildDocuments ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
to={`${to}?starred`}
icon={
depth === 0 ? (
if (documentId) {
const document = documents.get(documentId);
if (!document) {
return null;
}
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)
: [];
const hasChildDocuments = childDocuments.length > 0;
return (
<>
<Draggable key={star.id} ref={drag} $isDragging={isDragging}>
<SidebarLink
depth={0}
to={{
pathname: document.url,
state: { starred: true },
}}
expanded={hasChildDocuments && !isDragging ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
icon={
emoji ? (
<EmojiIcon emoji={emoji} />
) : (
<StarredIcon color={theme.yellow} />
)
) : undefined
}
isActive={(match, location) =>
!!match && location.search === "?starred"
}
label={depth === 0 ? label : title}
exact={false}
showActions={menuOpen}
menu={
document ? (
<Fade>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Fade>
) : undefined
}
/>
{isDraggingAny && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</Draggable>
{expanded &&
childDocuments.map((childDocument) => (
<ObserveredStarredLink
key={childDocument.id}
depth={depth === 0 ? 2 : depth + 1}
title={childDocument.title}
to={childDocument.url}
documentId={childDocument.id}
collectionId={collectionId}
}
isActive={(match, location: Location<{ starred?: boolean }>) =>
!!match && location.state?.starred === true
}
label={label}
exact={false}
showActions={menuOpen}
menu={
document && !isDragging ? (
<Fade>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Fade>
) : undefined
}
/>
))}
</>
);
</Draggable>
<Relative>
<Folder expanded={displayChildDocuments}>
{childDocuments.map((node, index) => (
<DocumentLink
key={node.id}
node={node}
collection={collection}
activeDocument={documents.active}
isDraft={node.isDraft}
depth={2}
index={index}
/>
))}
</Folder>
{isDraggingAny && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</Relative>
</>
);
}
if (collection) {
return (
<>
<Draggable key={star?.id} ref={drag} $isDragging={isDragging}>
<CollectionLink
collection={collection}
expanded={isDragging ? undefined : displayChildDocuments}
activeDocument={documents.active}
onDisclosureClick={handleDisclosureClick}
isDraggingAnyCollection={isDraggingAny}
/>
</Draggable>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={displayChildDocuments}
/>
{isDraggingAny && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</Relative>
</>
);
}
return null;
}
const Draggable = styled.div<{ $isDragging?: boolean }>`
@@ -152,6 +205,4 @@ const Draggable = styled.div<{ $isDragging?: boolean }>`
opacity: ${(props) => (props.$isDragging ? 0.5 : 1)};
`;
const ObserveredStarredLink = observer(StarredLink);
export default ObserveredStarredLink;
export default observer(StarredLink);