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:
@@ -1,18 +1,15 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { Location } from "history";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentReparent from "~/scenes/DocumentReparent";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import Fade from "~/components/Fade";
|
||||
import Modal from "~/components/Modal";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { createDocument } from "~/actions/definitions/documents";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
@@ -21,40 +18,36 @@ import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import CollectionMenu from "~/menus/CollectionMenu";
|
||||
import { NavigationNode } from "~/types";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import Relative from "./Relative";
|
||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
||||
import { useStarredContext } from "./StarredContext";
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
canUpdate: boolean;
|
||||
activeDocument: Document | null | undefined;
|
||||
prefetchDocument: (id: string) => Promise<Document | void>;
|
||||
belowCollection: Collection | void;
|
||||
expanded?: boolean;
|
||||
onDisclosureClick: (ev: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
activeDocument: Document | undefined;
|
||||
isDraggingAnyCollection?: boolean;
|
||||
};
|
||||
|
||||
function CollectionLink({
|
||||
const CollectionLink: React.FC<Props> = ({
|
||||
collection,
|
||||
activeDocument,
|
||||
prefetchDocument,
|
||||
canUpdate,
|
||||
belowCollection,
|
||||
}: Props) {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const [
|
||||
permissionOpen,
|
||||
handlePermissionOpen,
|
||||
handlePermissionClose,
|
||||
] = useBoolean();
|
||||
expanded,
|
||||
onDisclosureClick,
|
||||
isDraggingAnyCollection,
|
||||
}) => {
|
||||
const itemRef = React.useRef<
|
||||
NavigationNode & { depth: number; active: boolean; collectionId: string }
|
||||
>();
|
||||
const { dialogs, documents, collections } = useStores();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const canUpdate = usePolicy(collection.id).update;
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const inStarredSection = useStarredContext();
|
||||
|
||||
const handleTitleChange = React.useCallback(
|
||||
async (name: string) => {
|
||||
@@ -66,26 +59,6 @@ function CollectionLink({
|
||||
[collection, history]
|
||||
);
|
||||
|
||||
const handleTitleEditing = React.useCallback((isEditing: boolean) => {
|
||||
setIsEditing(isEditing);
|
||||
}, []);
|
||||
|
||||
const { ui, documents, collections } = useStores();
|
||||
const [expanded, setExpanded] = React.useState(
|
||||
collection.id === ui.activeCollectionId
|
||||
);
|
||||
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setOpenedOnce(true);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
const manualSort = collection.sort.field === "index";
|
||||
const can = usePolicy(collection.id);
|
||||
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
|
||||
|
||||
// Drop to re-parent document
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: "document",
|
||||
@@ -111,14 +84,23 @@ function CollectionLink({
|
||||
prevCollection.permission !== collection.permission
|
||||
) {
|
||||
itemRef.current = item;
|
||||
handlePermissionOpen();
|
||||
|
||||
dialogs.openModal({
|
||||
title: t("Move document"),
|
||||
content: (
|
||||
<DocumentReparent
|
||||
item={item}
|
||||
collection={collection}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
onCancel={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
documents.move(id, collection.id);
|
||||
}
|
||||
},
|
||||
canDrop: () => {
|
||||
return can.update;
|
||||
},
|
||||
canDrop: () => canUpdate,
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver({
|
||||
shallow: true,
|
||||
@@ -127,224 +109,69 @@ function CollectionLink({
|
||||
}),
|
||||
});
|
||||
|
||||
// Drop to reorder document
|
||||
const [{ isOverReorder }, dropToReorder] = useDrop({
|
||||
accept: "document",
|
||||
drop: (item: DragObject) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
documents.move(item.id, collection.id, undefined, 0);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: !!monitor.isOver(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Drop to reorder collection
|
||||
const [
|
||||
{ isCollectionDropping, isDraggingAnyCollection },
|
||||
dropToReorderCollection,
|
||||
] = useDrop({
|
||||
accept: "collection",
|
||||
drop: (item: DragObject) => {
|
||||
collections.move(
|
||||
item.id,
|
||||
fractionalIndex(collection.index, belowCollectionIndex)
|
||||
);
|
||||
},
|
||||
canDrop: (item) => {
|
||||
return (
|
||||
collection.id !== item.id &&
|
||||
(!belowCollection || item.id !== belowCollection.id)
|
||||
);
|
||||
},
|
||||
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
|
||||
isCollectionDropping: monitor.isOver(),
|
||||
isDraggingAnyCollection: monitor.getItemType() === "collection",
|
||||
}),
|
||||
});
|
||||
|
||||
// Drag to reorder collection
|
||||
const [{ isCollectionDragging }, dragToReorderCollection] = useDrag({
|
||||
type: "collection",
|
||||
item: () => {
|
||||
return {
|
||||
id: collection.id,
|
||||
};
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isCollectionDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => {
|
||||
return can.move;
|
||||
},
|
||||
});
|
||||
|
||||
const collectionDocuments = React.useMemo(() => {
|
||||
if (
|
||||
activeDocument?.isActive &&
|
||||
activeDocument?.isDraft &&
|
||||
activeDocument?.collectionId === collection.id &&
|
||||
!activeDocument?.parentDocumentId
|
||||
) {
|
||||
return sortNavigationNodes(
|
||||
[activeDocument.asNavigationNode, ...collection.documents],
|
||||
collection.sort
|
||||
);
|
||||
}
|
||||
|
||||
return collection.documents;
|
||||
}, [
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.collectionId,
|
||||
activeDocument?.parentDocumentId,
|
||||
activeDocument?.asNavigationNode,
|
||||
collection.documents,
|
||||
collection.id,
|
||||
collection.sort,
|
||||
]);
|
||||
|
||||
const displayDocumentLinks = expanded && !isCollectionDragging;
|
||||
|
||||
React.useEffect(() => {
|
||||
// If we're viewing a starred document through the starred menu then don't
|
||||
// touch the expanded / collapsed state of the collections
|
||||
if (search === "?starred") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection.id === ui.activeCollectionId) {
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [collection.id, ui.activeCollectionId, search]);
|
||||
const handleTitleEditing = React.useCallback((isEditing: boolean) => {
|
||||
setIsEditing(isEditing);
|
||||
}, []);
|
||||
|
||||
const context = useActionContext({
|
||||
activeCollectionId: collection.id,
|
||||
inStarredSection,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Relative ref={drop}>
|
||||
<Draggable
|
||||
key={collection.id}
|
||||
ref={dragToReorderCollection}
|
||||
$isDragging={isCollectionDragging}
|
||||
$isMoving={isCollectionDragging}
|
||||
>
|
||||
<DropToImport collectionId={collection.id}>
|
||||
<SidebarLink
|
||||
to={collection.url}
|
||||
expanded={displayDocumentLinks}
|
||||
onDisclosureClick={(event) => {
|
||||
event.preventDefault();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
icon={
|
||||
<CollectionIcon
|
||||
collection={collection}
|
||||
expanded={displayDocumentLinks}
|
||||
/>
|
||||
}
|
||||
showActions={menuOpen}
|
||||
isActiveDrop={isOver && canDrop}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={collection.name}
|
||||
onSubmit={handleTitleChange}
|
||||
onEditing={handleTitleEditing}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
depth={0}
|
||||
menu={
|
||||
!isEditing &&
|
||||
!isDraggingAnyCollection && (
|
||||
<Fade>
|
||||
<NudeButton
|
||||
tooltip={{ tooltip: t("New doc"), delay: 500 }}
|
||||
action={createDocument}
|
||||
context={context}
|
||||
hideOnActionDisabled
|
||||
>
|
||||
<PlusIcon />
|
||||
</NudeButton>
|
||||
<CollectionMenu
|
||||
collection={collection}
|
||||
onOpen={handleMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
/>
|
||||
</Fade>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</DropToImport>
|
||||
</Draggable>
|
||||
</Relative>
|
||||
<Relative>
|
||||
{openedOnce && (
|
||||
<Folder $open={displayDocumentLinks}>
|
||||
{manualSort && (
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{collectionDocuments.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
collection={collection}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
<DropToImport collectionId={collection.id}>
|
||||
<SidebarLink
|
||||
to={{
|
||||
pathname: collection.url,
|
||||
state: { starred: inStarredSection },
|
||||
}}
|
||||
expanded={expanded}
|
||||
onDisclosureClick={onDisclosureClick}
|
||||
icon={
|
||||
<CollectionIcon collection={collection} expanded={expanded} />
|
||||
}
|
||||
showActions={menuOpen}
|
||||
isActiveDrop={isOver && canDrop}
|
||||
isActive={(match, location: Location<{ starred?: boolean }>) =>
|
||||
!!match && location.state?.starred === inStarredSection
|
||||
}
|
||||
label={
|
||||
<EditableTitle
|
||||
title={collection.name}
|
||||
onSubmit={handleTitleChange}
|
||||
onEditing={handleTitleEditing}
|
||||
canUpdate={canUpdate}
|
||||
isDraft={node.isDraft}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
)}
|
||||
{isDraggingAnyCollection && (
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
}
|
||||
exact={false}
|
||||
depth={0}
|
||||
menu={
|
||||
!isEditing &&
|
||||
!isDraggingAnyCollection && (
|
||||
<Fade>
|
||||
<NudeButton
|
||||
tooltip={{ tooltip: t("New doc"), delay: 500 }}
|
||||
action={createDocument}
|
||||
context={context}
|
||||
hideOnActionDisabled
|
||||
>
|
||||
<PlusIcon />
|
||||
</NudeButton>
|
||||
<CollectionMenu
|
||||
collection={collection}
|
||||
onOpen={handleMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
/>
|
||||
</Fade>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DropToImport>
|
||||
</Relative>
|
||||
|
||||
<Modal
|
||||
title={t("Move document")}
|
||||
onRequestClose={handlePermissionClose}
|
||||
isOpen={permissionOpen}
|
||||
>
|
||||
{itemRef.current && (
|
||||
<DocumentReparent
|
||||
item={itemRef.current}
|
||||
collection={collection}
|
||||
onSubmit={handlePermissionClose}
|
||||
onCancel={handlePermissionClose}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Folder = styled.div<{ $open?: boolean }>`
|
||||
display: ${(props) => (props.$open ? "block" : "none")};
|
||||
`;
|
||||
|
||||
const Draggable = styled("div")<{ $isDragging: boolean; $isMoving: boolean }>`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "auto")};
|
||||
`;
|
||||
};
|
||||
|
||||
export default observer(CollectionLink);
|
||||
|
||||
91
app/components/Sidebar/components/CollectionLinkChildren.tsx
Normal file
91
app/components/Sidebar/components/CollectionLinkChildren.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import DocumentLink from "./DocumentLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import EmptyCollectionPlaceholder from "./EmptyCollectionPlaceholder";
|
||||
import Folder from "./Folder";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
import useCollectionDocuments from "./useCollectionDocuments";
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
expanded: boolean;
|
||||
prefetchDocument?: (documentId: string) => Promise<Document | void>;
|
||||
};
|
||||
|
||||
function CollectionLinkChildren({
|
||||
collection,
|
||||
expanded,
|
||||
prefetchDocument,
|
||||
}: Props) {
|
||||
const can = usePolicy(collection.id);
|
||||
const { showToast } = useToasts();
|
||||
const manualSort = collection.sort.field === "index";
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const childDocuments = useCollectionDocuments(collection, documents.active);
|
||||
|
||||
// Drop to reorder document
|
||||
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
|
||||
accept: "document",
|
||||
drop: (item: DragObject) => {
|
||||
if (!manualSort) {
|
||||
showToast(
|
||||
t(
|
||||
"You can't reorder documents in an alphabetically sorted collection"
|
||||
),
|
||||
{
|
||||
type: "info",
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
documents.move(item.id, collection.id, undefined, 0);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: !!monitor.isOver(),
|
||||
isDraggingAnyDocument: !!monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Folder expanded={expanded}>
|
||||
{isDraggingAnyDocument && can.update && (
|
||||
<DropCursor
|
||||
disabled={!manualSort}
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{childDocuments.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={prefetchDocument}
|
||||
isDraft={node.isDraft}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{childDocuments.length === 0 && <EmptyCollectionPlaceholder />}
|
||||
</Folder>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(CollectionLinkChildren);
|
||||
@@ -3,24 +3,24 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
import { createCollection } from "~/actions/definitions/collections";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import DraggableCollectionLink from "./DraggableCollectionLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Relative from "./Relative";
|
||||
import SidebarAction from "./SidebarAction";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
|
||||
function Collections() {
|
||||
const [isFetching, setFetching] = React.useState(false);
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const { policies, documents, collections } = useStores();
|
||||
const { documents, collections } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const isPreloaded = !!collections.orderedData.length;
|
||||
@@ -82,12 +82,11 @@ function Collections() {
|
||||
/>
|
||||
)}
|
||||
{orderedCollections.map((collection: Collection, index: number) => (
|
||||
<CollectionLink
|
||||
<DraggableCollectionLink
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={documents.prefetchDocument}
|
||||
canUpdate={policies.abilities(collection.id).update}
|
||||
belowCollection={orderedCollections[index + 1]}
|
||||
/>
|
||||
))}
|
||||
@@ -116,8 +115,4 @@ function Collections() {
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default observer(Collections);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Location } from "history";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -13,6 +14,7 @@ import Fade from "~/components/Fade";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
@@ -21,24 +23,25 @@ import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import Folder from "./Folder";
|
||||
import Relative from "./Relative";
|
||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
||||
import { useStarredContext } from "./StarredContext";
|
||||
|
||||
type Props = {
|
||||
node: NavigationNode;
|
||||
canUpdate: boolean;
|
||||
collection?: Collection;
|
||||
activeDocument: Document | null | undefined;
|
||||
prefetchDocument: (documentId: string) => Promise<Document | void>;
|
||||
prefetchDocument?: (documentId: string) => Promise<Document | void>;
|
||||
isDraft?: boolean;
|
||||
depth: number;
|
||||
index: number;
|
||||
parentId?: string;
|
||||
};
|
||||
|
||||
function DocumentLink(
|
||||
function InnerDocumentLink(
|
||||
{
|
||||
node,
|
||||
canUpdate,
|
||||
collection,
|
||||
activeDocument,
|
||||
prefetchDocument,
|
||||
@@ -52,12 +55,14 @@ function DocumentLink(
|
||||
const { showToast } = useToasts();
|
||||
const { documents, policies } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const canUpdate = usePolicy(node.id).update;
|
||||
const isActiveDocument = activeDocument && activeDocument.id === node.id;
|
||||
const hasChildDocuments =
|
||||
!!node.children.length || activeDocument?.parentDocumentId === node.id;
|
||||
const document = documents.get(node.id);
|
||||
const { fetchChildDocuments } = documents;
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const inStarredSection = useStarredContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isActiveDocument && hasChildDocuments) {
|
||||
@@ -84,7 +89,6 @@ function DocumentLink(
|
||||
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
|
||||
|
||||
const [expanded, setExpanded] = React.useState(showChildren);
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showChildren) {
|
||||
@@ -92,14 +96,7 @@ function DocumentLink(
|
||||
}
|
||||
}, [showChildren]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setOpenedOnce(true);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
// when the last child document is removed,
|
||||
// also close the local folder state to closed
|
||||
// when the last child document is removed auto-close the local folder state
|
||||
React.useEffect(() => {
|
||||
if (expanded && !hasChildDocuments) {
|
||||
setExpanded(false);
|
||||
@@ -116,7 +113,7 @@ function DocumentLink(
|
||||
);
|
||||
|
||||
const handleMouseEnter = React.useCallback(() => {
|
||||
prefetchDocument(node.id);
|
||||
prefetchDocument?.(node.id);
|
||||
}, [prefetchDocument, node]);
|
||||
|
||||
const handleTitleChange = React.useCallback(
|
||||
@@ -190,7 +187,7 @@ function DocumentLink(
|
||||
!isDraft &&
|
||||
!!pathToNode &&
|
||||
!pathToNode.includes(monitor.getItem<DragObject>().id),
|
||||
hover: (item, monitor) => {
|
||||
hover: (_item, monitor) => {
|
||||
// Enables expansion of document children when hovering over the document
|
||||
// for more than half a second.
|
||||
if (
|
||||
@@ -314,6 +311,7 @@ function DocumentLink(
|
||||
pathname: node.url,
|
||||
state: {
|
||||
title: node.title,
|
||||
starred: inStarredSection,
|
||||
},
|
||||
}}
|
||||
label={
|
||||
@@ -325,14 +323,14 @@ function DocumentLink(
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
/>
|
||||
}
|
||||
isActive={(match, location) =>
|
||||
!!match && location.search !== "?starred"
|
||||
isActive={(match, location: Location<{ starred?: boolean }>) =>
|
||||
!!match && location.state?.starred === inStarredSection
|
||||
}
|
||||
isActiveDrop={isOverReparent && canDropToReparent}
|
||||
depth={depth}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
scrollIntoViewIfNeeded={!document?.isStarred}
|
||||
scrollIntoViewIfNeeded={!inStarredSection}
|
||||
isDraft={isDraft}
|
||||
ref={ref}
|
||||
menu={
|
||||
@@ -375,41 +373,30 @@ function DocumentLink(
|
||||
/>
|
||||
)}
|
||||
</Relative>
|
||||
{openedOnce && (
|
||||
<Folder $open={expanded && !isDragging}>
|
||||
{nodeChildren.map((childNode, index) => (
|
||||
<ObservedDocumentLink
|
||||
key={childNode.id}
|
||||
collection={collection}
|
||||
node={childNode}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
isDraft={childNode.isDraft}
|
||||
depth={depth + 1}
|
||||
canUpdate={canUpdate}
|
||||
index={index}
|
||||
parentId={node.id}
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
)}
|
||||
<Folder expanded={expanded && !isDragging}>
|
||||
{nodeChildren.map((childNode, index) => (
|
||||
<DocumentLink
|
||||
key={childNode.id}
|
||||
collection={collection}
|
||||
node={childNode}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
isDraft={childNode.isDraft}
|
||||
depth={depth + 1}
|
||||
index={index}
|
||||
parentId={node.id}
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Folder = styled.div<{ $open?: boolean }>`
|
||||
display: ${(props) => (props.$open ? "block" : "none")};
|
||||
`;
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||
`;
|
||||
|
||||
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
||||
const DocumentLink = observer(React.forwardRef(InnerDocumentLink));
|
||||
|
||||
export default ObservedDocumentLink;
|
||||
export default DocumentLink;
|
||||
|
||||
148
app/components/Sidebar/components/DraggableCollectionLink.tsx
Normal file
148
app/components/Sidebar/components/DraggableCollectionLink.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import CollectionLinkChildren from "./CollectionLinkChildren";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Relative from "./Relative";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
import { useStarredContext } from "./StarredContext";
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
activeDocument: Document | undefined;
|
||||
prefetchDocument: (id: string) => Promise<Document | void>;
|
||||
belowCollection: Collection | void;
|
||||
};
|
||||
|
||||
function useLocationStateStarred() {
|
||||
const location = useLocation<{
|
||||
starred?: boolean;
|
||||
}>();
|
||||
return location.state?.starred;
|
||||
}
|
||||
|
||||
function DraggableCollectionLink({
|
||||
collection,
|
||||
activeDocument,
|
||||
prefetchDocument,
|
||||
belowCollection,
|
||||
}: Props) {
|
||||
const locationStateStarred = useLocationStateStarred();
|
||||
const { ui, collections } = useStores();
|
||||
const inStarredSection = useStarredContext();
|
||||
const [expanded, setExpanded] = React.useState(
|
||||
collection.id === ui.activeCollectionId &&
|
||||
locationStateStarred === inStarredSection
|
||||
);
|
||||
const can = usePolicy(collection.id);
|
||||
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
|
||||
|
||||
// Drop to reorder collection
|
||||
const [
|
||||
{ isCollectionDropping, isDraggingAnyCollection },
|
||||
dropToReorderCollection,
|
||||
] = useDrop({
|
||||
accept: "collection",
|
||||
drop: (item: DragObject) => {
|
||||
collections.move(
|
||||
item.id,
|
||||
fractionalIndex(collection.index, belowCollectionIndex)
|
||||
);
|
||||
},
|
||||
canDrop: (item) => {
|
||||
return (
|
||||
collection.id !== item.id &&
|
||||
(!belowCollection || item.id !== belowCollection.id)
|
||||
);
|
||||
},
|
||||
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
|
||||
isCollectionDropping: monitor.isOver(),
|
||||
isDraggingAnyCollection: monitor.getItemType() === "collection",
|
||||
}),
|
||||
});
|
||||
|
||||
// Drag to reorder collection
|
||||
const [{ isCollectionDragging }, dragToReorderCollection] = useDrag({
|
||||
type: "collection",
|
||||
item: () => {
|
||||
return {
|
||||
id: collection.id,
|
||||
};
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isCollectionDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => {
|
||||
return can.move;
|
||||
},
|
||||
});
|
||||
|
||||
// If the current collection is active and relevant to the sidebar section we
|
||||
// are in then expand it automatically
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
collection.id === ui.activeCollectionId &&
|
||||
locationStateStarred === inStarredSection
|
||||
) {
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [
|
||||
collection.id,
|
||||
ui.activeCollectionId,
|
||||
locationStateStarred,
|
||||
inStarredSection,
|
||||
]);
|
||||
|
||||
const handleDisclosureClick = React.useCallback((ev) => {
|
||||
ev.preventDefault();
|
||||
setExpanded((e) => !e);
|
||||
}, []);
|
||||
|
||||
const displayChildDocuments = expanded && !isCollectionDragging;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Draggable
|
||||
key={collection.id}
|
||||
ref={dragToReorderCollection}
|
||||
$isDragging={isCollectionDragging}
|
||||
>
|
||||
<CollectionLink
|
||||
collection={collection}
|
||||
expanded={displayChildDocuments}
|
||||
activeDocument={activeDocument}
|
||||
onDisclosureClick={handleDisclosureClick}
|
||||
isDraggingAnyCollection={isDraggingAnyCollection}
|
||||
/>
|
||||
</Draggable>
|
||||
<Relative>
|
||||
<CollectionLinkChildren
|
||||
collection={collection}
|
||||
expanded={displayChildDocuments}
|
||||
prefetchDocument={prefetchDocument}
|
||||
/>
|
||||
{isDraggingAnyCollection && (
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
/>
|
||||
)}
|
||||
</Relative>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Draggable = styled("div")<{ $isDragging: boolean }>`
|
||||
opacity: ${(props) => (props.$isDragging ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isDragging ? "none" : "auto")};
|
||||
`;
|
||||
|
||||
export default observer(DraggableCollectionLink);
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
const EmptyCollectionPlaceholder = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Empty type="tertiary" size="small">
|
||||
{t("Empty")}
|
||||
</Empty>
|
||||
);
|
||||
};
|
||||
|
||||
const Empty = styled(Text)`
|
||||
margin-left: 46px;
|
||||
margin-bottom: 0;
|
||||
line-height: 34px;
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
export default EmptyCollectionPlaceholder;
|
||||
29
app/components/Sidebar/components/Folder.tsx
Normal file
29
app/components/Sidebar/components/Folder.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
expanded: boolean;
|
||||
};
|
||||
|
||||
const Folder: React.FC<Props> = ({ expanded, children }) => {
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
|
||||
// allows us to avoid rendering all children when the folder hasn't been opened
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setOpenedOnce(true);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
if (!openedOnce) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Wrapper $expanded={expanded}>{children}</Wrapper>;
|
||||
};
|
||||
|
||||
const Wrapper = styled.div<{ $expanded?: boolean }>`
|
||||
display: ${(props) => (props.$expanded ? "block" : "none")};
|
||||
`;
|
||||
|
||||
export default Folder;
|
||||
7
app/components/Sidebar/components/Relative.tsx
Normal file
7
app/components/Sidebar/components/Relative.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default Relative;
|
||||
@@ -3,7 +3,6 @@ import { observer } from "mobx-react";
|
||||
import * as React 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";
|
||||
@@ -11,7 +10,9 @@ import useToasts from "~/hooks/useToasts";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Relative from "./Relative";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import StarredContext from "./StarredContext";
|
||||
import StarredLink from "./StarredLink";
|
||||
|
||||
const STARRED_PAGINATION_LIMIT = 10;
|
||||
@@ -25,7 +26,7 @@ function Starred() {
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
|
||||
const { showToast } = useToasts();
|
||||
const { stars, documents } = useStores();
|
||||
const { stars } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
@@ -123,59 +124,45 @@ function Starred() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Header onClick={handleExpandClick} expanded={expanded}>
|
||||
{t("Starred")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Relative>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{stars.orderedData.slice(0, upperBound).map((star) => {
|
||||
const document = documents.get(star.documentId);
|
||||
|
||||
return document ? (
|
||||
<StarredLink
|
||||
key={star.id}
|
||||
star={star}
|
||||
documentId={document.id}
|
||||
collectionId={document.collectionId}
|
||||
to={document.url}
|
||||
title={document.title}
|
||||
<StarredContext.Provider value={true}>
|
||||
<Flex column>
|
||||
<Header onClick={handleExpandClick} expanded={expanded}>
|
||||
{t("Starred")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Relative>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{stars.orderedData.slice(0, upperBound).map((star) => (
|
||||
<StarredLink key={star.id} star={star} />
|
||||
))}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
depth={0}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</Relative>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</Relative>
|
||||
)}
|
||||
</Flex>
|
||||
</StarredContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default observer(Starred);
|
||||
|
||||
7
app/components/Sidebar/components/StarredContext.ts
Normal file
7
app/components/Sidebar/components/StarredContext.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
const StarredContext = React.createContext<boolean | undefined>(undefined);
|
||||
|
||||
export const useStarredContext = () => React.useContext(StarredContext);
|
||||
|
||||
export default StarredContext;
|
||||
@@ -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);
|
||||
|
||||
39
app/components/Sidebar/components/useCollectionDocuments.ts
Normal file
39
app/components/Sidebar/components/useCollectionDocuments.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as React from "react";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
|
||||
export default function useCollectionDocuments(
|
||||
collection: Collection | undefined,
|
||||
activeDocument: Document | undefined
|
||||
) {
|
||||
return React.useMemo(() => {
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
activeDocument?.isActive &&
|
||||
activeDocument?.isDraft &&
|
||||
activeDocument?.collectionId === collection.id &&
|
||||
!activeDocument?.parentDocumentId
|
||||
) {
|
||||
return sortNavigationNodes(
|
||||
[activeDocument.asNavigationNode, ...collection.documents],
|
||||
collection.sort
|
||||
);
|
||||
}
|
||||
|
||||
return collection.documents;
|
||||
}, [
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.collectionId,
|
||||
activeDocument?.parentDocumentId,
|
||||
activeDocument?.asNavigationNode,
|
||||
collection,
|
||||
collection?.documents,
|
||||
collection?.id,
|
||||
collection?.sort,
|
||||
]);
|
||||
}
|
||||
@@ -1,35 +1,56 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon, UnstarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import {
|
||||
starCollection,
|
||||
unstarCollection,
|
||||
} from "~/actions/definitions/collections";
|
||||
import { starDocument, unstarDocument } from "~/actions/definitions/documents";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import { hover } from "~/styles";
|
||||
import NudeButton from "./NudeButton";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
collection?: Collection;
|
||||
document?: Document;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
function Star({ size, document, ...rest }: Props) {
|
||||
function Star({ size, document, collection, ...rest }: Props) {
|
||||
const theme = useTheme();
|
||||
const context = useActionContext({
|
||||
activeDocumentId: document.id,
|
||||
activeDocumentId: document?.id,
|
||||
activeCollectionId: collection?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
const target = document || collection;
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NudeButton
|
||||
context={context}
|
||||
action={document.isStarred ? unstarDocument : starDocument}
|
||||
hideOnActionDisabled
|
||||
action={
|
||||
collection
|
||||
? collection.isStarred
|
||||
? unstarCollection
|
||||
: starCollection
|
||||
: document
|
||||
? document.isStarred
|
||||
? unstarDocument
|
||||
: starDocument
|
||||
: undefined
|
||||
}
|
||||
size={size}
|
||||
{...rest}
|
||||
>
|
||||
{document.isStarred ? (
|
||||
{target.isStarred ? (
|
||||
<AnimatedStar size={size} color={theme.yellow} />
|
||||
) : (
|
||||
<AnimatedStar
|
||||
@@ -58,4 +79,4 @@ export const AnimatedStar = styled(StarredIcon)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Star;
|
||||
export default observer(Star);
|
||||
|
||||
Reference in New Issue
Block a user