diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx index 76533bc946..6b2f2b3b24 100644 --- a/app/components/DocumentCard.tsx +++ b/app/components/DocumentCard.tsx @@ -110,7 +110,7 @@ function DocumentCard(props: Props) { dir={document.dir} $isDragging={isDragging} to={{ - pathname: document.url, + pathname: document.path, state: { title: document.titleWithDefault, }, diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index 97e77b6815..e96bee59e4 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -1,36 +1,22 @@ -import type { Location } from "history"; import { observer } from "mobx-react"; -import { PlusIcon } from "outline-icons"; import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { mergeRefs } from "react-merge-refs"; import { useHistory } from "react-router-dom"; import { UserPreference } from "@shared/types"; import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper"; -import { CollectionValidation, DocumentValidation } from "@shared/validations"; import type Collection from "~/models/Collection"; import type Document from "~/models/Document"; import type { RefHandle } from "~/components/EditableTitle"; -import EditableTitle from "~/components/EditableTitle"; -import Fade from "~/components/Fade"; -import CollectionIcon from "~/components/Icons/CollectionIcon"; -import NudeButton from "~/components/NudeButton"; -import Tooltip from "~/components/Tooltip"; -import useBoolean from "~/hooks/useBoolean"; import useCurrentUser from "~/hooks/useCurrentUser"; +import { useCollectionMenuAction } from "~/hooks/useCollectionMenuAction"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import CollectionMenu from "~/menus/CollectionMenu"; +import useBoolean from "~/hooks/useBoolean"; import { documentEditPath } from "~/utils/routeHelpers"; import { useDropToChangeCollection } from "../hooks/useDragAndDrop"; -import DropToImport from "./DropToImport"; -import Relative from "./Relative"; -import type { SidebarContextType } from "./SidebarContext"; -import { useSidebarContext } from "./SidebarContext"; -import SidebarLink from "./SidebarLink"; -import { useCollectionMenuAction } from "~/hooks/useCollectionMenuAction"; -import { ActionContextProvider } from "~/hooks/useActionContext"; import CollectionLinkChildren from "./CollectionLinkChildren"; +import CollectionRow from "./CollectionRow"; +import { useSidebarContext } from "./SidebarContext"; type Props = { collection: Collection; @@ -51,20 +37,16 @@ const CollectionLink: React.FC = ({ onClick, }: Props) => { const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); - const [isEditing, setIsEditing] = React.useState(false); const { documents } = useStores(); const history = useHistory(); const can = usePolicy(collection); - const { t } = useTranslation(); const sidebarContext = useSidebarContext(); const user = useCurrentUser(); const editableTitleRef = React.useRef(null); const handleTitleChange = React.useCallback( async (name: string) => { - await collection.save({ - name, - }); + await collection.save({ name }); }, [collection] ); @@ -88,37 +70,26 @@ const CollectionLink: React.FC = ({ const handleRename = React.useCallback(() => { editableTitleRef.current?.setIsEditing(true); - }, [editableTitleRef]); - - const newChildTitleRef = React.useRef(null); - const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] = - useBoolean(); + }, []); const handleNewDoc = React.useCallback( - async (input) => { - try { - newChildTitleRef.current?.setIsEditing(false); - const newDocument = await documents.create( - { - collectionId: collection.id, - title: input, - fullWidth: user.getPreference(UserPreference.FullWidthDocuments), - data: ProsemirrorHelper.getEmptyDocument(), - }, - { publish: true } - ); - collection?.addDocument(newDocument); - - closeAddingNewChild(); - history.push({ - pathname: documentEditPath(newDocument), - state: { sidebarContext }, - }); - } catch (_err) { - newChildTitleRef.current?.setIsEditing(true); - } + async (input: string) => { + const newDocument = await documents.create( + { + collectionId: collection.id, + title: input, + fullWidth: user.getPreference(UserPreference.FullWidthDocuments), + data: ProsemirrorHelper.getEmptyDocument(), + }, + { publish: true } + ); + collection?.addDocument(newDocument); + history.push({ + pathname: documentEditPath(newDocument), + state: { sidebarContext }, + }); }, - [user, sidebarContext, closeAddingNewChild, history, collection, documents] + [user, sidebarContext, history, collection, documents] ); const contextMenuAction = useCollectionMenuAction({ @@ -126,98 +97,44 @@ const CollectionLink: React.FC = ({ onRename: handleRename, }); + const menu = !isDraggingAnyCollection ? ( + + ) : undefined; + return ( - - - - - } - $showActions={menuOpen} - isActiveDrop={isOver && canDrop} - isActive={( - match, - location: Location<{ sidebarContext?: SidebarContextType }> - ) => !!match && location.state?.sidebarContext === sidebarContext} - label={ - - } - ellipsis={!isEditing} - exact={false} - depth={depth ? depth : 0} - menu={ - !isEditing && - !isDraggingAnyCollection && ( - - {can.createDocument && ( - - { - ev.preventDefault(); - setIsAddingNewChild(); - handleExpand(); - }} - > - - - - )} - - - ) - } - /> - - + - {isAddingNewChild ? ( - true} - ellipsis={false} - label={ - - } - /> - ) : undefined} - - + /> + ); }; diff --git a/app/components/Sidebar/components/CollectionRow.tsx b/app/components/Sidebar/components/CollectionRow.tsx new file mode 100644 index 0000000000..9518bbd10a --- /dev/null +++ b/app/components/Sidebar/components/CollectionRow.tsx @@ -0,0 +1,260 @@ +import type { Location, LocationDescriptor } from "history"; +import { observer } from "mobx-react"; +import { PlusIcon } from "outline-icons"; +import * as React from "react"; +import type { ConnectDropTarget } from "react-dnd"; +import { useTranslation } from "react-i18next"; +import { mergeRefs } from "react-merge-refs"; +import type { match } from "react-router"; +import { CollectionValidation, DocumentValidation } from "@shared/validations"; +import type Collection from "~/models/Collection"; +import EditableTitle, { type RefHandle } from "~/components/EditableTitle"; +import Fade from "~/components/Fade"; +import CollectionIcon from "~/components/Icons/CollectionIcon"; +import NudeButton from "~/components/NudeButton"; +import Tooltip from "~/components/Tooltip"; +import useBoolean from "~/hooks/useBoolean"; +import { ActionContextProvider } from "~/hooks/useActionContext"; +import DropToImport from "./DropToImport"; +import Relative from "./Relative"; +import SidebarLink from "./SidebarLink"; +import type { SidebarContextType } from "./SidebarContext"; +import { useSidebarContext } from "./SidebarContext"; +import type { ActionWithChildren } from "~/types"; + +export type CollectionRowProps = { + /** Collection model for the row. */ + collection: Collection; + /** Indentation depth of the row. */ + depth?: number; + + /** Navigation target for the row. */ + to: LocationDescriptor; + /** Click handler for the row. */ + onClick?: () => void; + /** Called on click intent for prefetching. */ + onClickIntent?: () => void; + /** Optional override for the active-match function. */ + isActiveOverride?: ( + match: match | null, + location: Location<{ sidebarContext?: SidebarContextType }> + ) => boolean; + + /** Icon displayed to the left of the label. Defaults to CollectionIcon. */ + icon?: React.ReactNode; + + /** Whether the row is expanded. Pass undefined to hide the disclosure. */ + expanded?: boolean; + /** Called when the disclosure caret toggles expansion. */ + onDisclosureClick: (ev?: React.MouseEvent) => void; + /** Imperative expand, used by the "+" button to auto-expand. */ + onExpand?: () => void; + + /** When true, the name renders as an EditableTitle. */ + canEdit?: boolean; + /** Title displayed and edited when canEdit is true. */ + labelText?: string; + /** Submit handler for the edited title. */ + onTitleChange?: (value: string) => Promise; + /** Forwarded ref to the EditableTitle so the container can trigger rename. */ + editableTitleRef?: React.Ref; + /** Notifies the container when the inline title's editing state changes. */ + onEditingChange?: (editing: boolean) => void; + + /** Context menu action for the row. */ + contextAction?: ActionWithChildren; + /** Menu content rendered by the container; wrapped in Fade. */ + menu?: React.ReactNode; + /** Whether the menu's action slot is visible (e.g. while the menu is open). */ + menuOpen?: boolean; + + /** When true, the "+" new-child button is rendered in the menu slot. */ + canCreateChild?: boolean; + /** Submit handler for the inline new-child title input. */ + onCreateChild?: (title: string) => Promise; + /** Depth of the inline new-child SidebarLink. Defaults to 2. */ + newChildDepth?: number; + + /** Ref forwarded to the outer Relative; for drag hover timers. */ + parentRef?: React.Ref; + /** Drop target connector for "change collection" / reorder. */ + dropRef?: ConnectDropTarget; + /** Whether the row is an active drop target (visual highlight). */ + isActiveDropTarget?: boolean; + + /** Content rendered after the row (e.g. CollectionLinkChildren). */ + children?: React.ReactNode; +}; + +function CollectionRow({ + collection, + depth = 0, + to, + onClick, + onClickIntent, + isActiveOverride, + icon, + expanded, + onDisclosureClick, + onExpand, + canEdit, + labelText, + onTitleChange, + editableTitleRef, + onEditingChange, + contextAction, + menu, + menuOpen, + canCreateChild, + onCreateChild, + newChildDepth = 2, + parentRef, + dropRef, + isActiveDropTarget, + children, +}: CollectionRowProps) { + const { t } = useTranslation(); + const sidebarContext = useSidebarContext(); + const [isEditing, setIsEditingState] = React.useState(false); + const setIsEditing = React.useCallback( + (editing: boolean) => { + setIsEditingState(editing); + onEditingChange?.(editing); + }, + [onEditingChange] + ); + const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] = + useBoolean(); + const newChildTitleRef = React.useRef(null); + + const handleAddChild = React.useCallback( + (ev: React.MouseEvent) => { + ev.preventDefault(); + setIsAddingNewChild(); + onExpand?.(); + }, + [setIsAddingNewChild, onExpand] + ); + + const handleNewChildSubmit = React.useCallback( + async (value: string) => { + if (!onCreateChild) { + return; + } + try { + newChildTitleRef.current?.setIsEditing(false); + await onCreateChild(value); + closeAddingNewChild(); + } catch (_err) { + newChildTitleRef.current?.setIsEditing(true); + } + }, + [onCreateChild, closeAddingNewChild] + ); + + const defaultIsActive = React.useCallback( + ( + _m: match | null, + location: Location<{ sidebarContext?: SidebarContextType }> + ) => !!_m && location.state?.sidebarContext === sidebarContext, + [sidebarContext] + ); + + const labelElement = canEdit ? ( + undefined)} + isEditing={isEditing} + onEditing={setIsEditing} + canUpdate={canEdit} + maxLength={CollectionValidation.maxNameLength} + ref={editableTitleRef} + /> + ) : ( + collection.name + ); + + const iconElement = icon ?? ( + + ); + + const hasMenuContent = Boolean(menu) || canCreateChild; + const menuVisible = hasMenuContent && !isEditing; + const menuElement = menuVisible ? ( + + {canCreateChild && ( + + + + + + )} + {menu} + + ) : undefined; + + const mergedRef = React.useMemo( + () => + mergeRefs( + [parentRef, dropRef].filter(Boolean) as React.Ref[] + ), + [parentRef, dropRef] + ); + + const sidebarLinkElement = ( + + ); + + return ( + + + + {sidebarLinkElement} + + + {isAddingNewChild && onCreateChild && ( + true} + depth={newChildDepth} + ellipsis={false} + label={ + + } + /> + )} + {children} + + ); +} + +export default observer(CollectionRow); diff --git a/app/components/Sidebar/components/DocumentLink.tsx b/app/components/Sidebar/components/DocumentLink.tsx index eaccd07cf1..6fbf74b7d7 100644 --- a/app/components/Sidebar/components/DocumentLink.tsx +++ b/app/components/Sidebar/components/DocumentLink.tsx @@ -1,25 +1,21 @@ import type { Location } from "history"; import { observer } from "mobx-react"; -import { PlusIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; -import styled from "styled-components"; import Icon from "@shared/components/Icon"; import type { NavigationNode } from "@shared/types"; import { UserPreference } from "@shared/types"; import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper"; import { sortNavigationNodes } from "@shared/utils/collections"; -import { DocumentValidation } from "@shared/validations"; import type Collection from "~/models/Collection"; import type Document from "~/models/Document"; +import type GroupMembership from "~/models/GroupMembership"; +import type UserMembership from "~/models/UserMembership"; import type { RefHandle } from "~/components/EditableTitle"; -import EditableTitle from "~/components/EditableTitle"; -import Fade from "~/components/Fade"; -import NudeButton from "~/components/NudeButton"; -import Tooltip from "~/components/Tooltip"; import useBoolean from "~/hooks/useBoolean"; import useCurrentUser from "~/hooks/useCurrentUser"; +import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import DocumentMenu from "~/menus/DocumentMenu"; @@ -29,17 +25,11 @@ import { useDropToReorderDocument, useDropToReparentDocument, } from "../hooks/useDragAndDrop"; +import DocumentRow from "./DocumentRow"; import DropCursor from "./DropCursor"; -import DropToImport from "./DropToImport"; import Folder from "./Folder"; -import Relative from "./Relative"; import type { SidebarContextType } from "./SidebarContext"; import { useSidebarContext } from "./SidebarContext"; -import SidebarLink from "./SidebarLink"; -import type UserMembership from "~/models/UserMembership"; -import type GroupMembership from "~/models/GroupMembership"; -import { ActionContextProvider } from "~/hooks/useActionContext"; -import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction"; import SidebarDisclosureContext, { useSidebarDisclosure, useSidebarDisclosureState, @@ -57,20 +47,17 @@ type Props = { parentId?: string; }; -function InnerDocumentLink( - { - node, - collection, - membership, - activeDocument, - prefetchDocument, - isDraft, - depth, - index, - parentId, - }: Props, - ref: React.RefObject -) { +const DocumentLink = observer(function DocumentLinkInner({ + node, + collection, + membership, + activeDocument, + prefetchDocument, + isDraft, + depth, + index, + parentId, +}: Props) { const { documents, policies } = useStores(); const { t } = useTranslation(); const history = useHistory(); @@ -123,11 +110,9 @@ function InnerDocumentLink( const [expanded, setExpanded, setCollapsed] = useBoolean(showChildren); - // Context-based recursive expand/collapse for descendant DocumentLinks const { event: disclosureEvent, onDisclosureClick } = useSidebarDisclosureState(); - // Subscribe to recursive expand/collapse events from an ancestor useSidebarDisclosure(setExpanded, setCollapsed); React.useEffect(() => { @@ -136,7 +121,6 @@ function InnerDocumentLink( } }, [setExpanded, showChildren]); - // when the last child document is removed auto-close the local folder state React.useEffect(() => { if (expanded && !hasChildDocuments) { setCollapsed(); @@ -144,14 +128,14 @@ function InnerDocumentLink( }, [setCollapsed, expanded, hasChildDocuments]); const handleDisclosureClick = React.useCallback( - (ev: React.MouseEvent) => { + (ev?: React.MouseEvent) => { const willExpand = !expanded; if (willExpand) { setExpanded(); } else { setCollapsed(); } - onDisclosureClick(willExpand, ev.altKey); + onDisclosureClick(willExpand, !!ev?.altKey); }, [setCollapsed, setExpanded, expanded, onDisclosureClick] ); @@ -172,6 +156,7 @@ function InnerDocumentLink( }, [documents, document] ); + const handleRename = React.useCallback(() => { editableTitleRef.current?.setIsEditing(true); }, []); @@ -214,10 +199,9 @@ function InnerDocumentLink( const iconElement = React.useMemo( () => icon ? : undefined, - [icon, color] + [icon, color, initial] ); - // Draggable const [{ isDragging }, drag] = useDragDocument( node, depth, @@ -225,12 +209,10 @@ function InnerDocumentLink( isEditing ); - // Drop to re-parent const parentRef = React.useRef(null); const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDropToReparentDocument(node, setExpanded, parentRef); - // Drop to reorder const [{ isOverReorder: isOverReorderAbove }, dropToReorderAbove] = useDropToReorderDocument(node, collection, (item) => { if (!collection) { @@ -271,9 +253,6 @@ function InnerDocumentLink( activeDocument?.parentDocumentId === node.id ); - // Only subscribe to asNavigationNode when this node is the parent of an - // active draft. This avoids every DocumentLink observer re-rendering on - // every title keystroke. const draftNavNode = insertDraftChild ? activeDocument?.asNavigationNode : undefined; @@ -292,66 +271,38 @@ function InnerDocumentLink( const doc = documents.get(node.id); const title = doc?.title || node.title || t("Untitled"); - - const isExpanded = expanded && !isDragging; const hasChildren = nodeChildren.length > 0; - const handleKeyDown = React.useCallback( - (ev: React.KeyboardEvent) => { - if (!hasChildren) { - return; - } - if (ev.key === "ArrowRight" && !expanded) { - setExpanded(); - } - if (ev.key === "ArrowLeft" && expanded) { - setCollapsed(); - } - }, - [setExpanded, setCollapsed, hasChildren, expanded] - ); - - const newChildTitleRef = React.useRef(null); - const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] = - useBoolean(); - const handleNewDoc = React.useCallback( - async (input) => { - try { - newChildTitleRef.current?.setIsEditing(false); - const newDocument = await documents.create( - { - collectionId: collection?.id, - parentDocumentId: node.id, - fullWidth: - doc?.fullWidth ?? - user.getPreference(UserPreference.FullWidthDocuments), - title: input, - data: ProsemirrorHelper.getEmptyDocument(), - }, - { publish: true } - ); - collection?.addDocument(newDocument, node.id); - membership?.addDocument(newDocument, node.id); - - closeAddingNewChild(); - history.push({ - pathname: documentEditPath(newDocument), - state: { sidebarContext }, - }); - } catch (_err) { - newChildTitleRef.current?.setIsEditing(true); - } + async (input: string) => { + const newDocument = await documents.create( + { + collectionId: collection?.id, + parentDocumentId: node.id, + fullWidth: + doc?.fullWidth ?? + user.getPreference(UserPreference.FullWidthDocuments), + title: input, + data: ProsemirrorHelper.getEmptyDocument(), + }, + { publish: true } + ); + collection?.addDocument(newDocument, node.id); + membership?.addDocument(newDocument, node.id); + history.push({ + pathname: documentEditPath(newDocument), + state: { sidebarContext }, + }); }, [ documents, collection, + membership, sidebarContext, user, node, doc, history, - closeAddingNewChild, ] ); @@ -360,132 +311,66 @@ function InnerDocumentLink( onRename: handleRename, }); - const labelElement = React.useMemo( - () => ( - - ), - [title, handleTitleChange, isEditing, setIsEditing, canUpdate] - ); + ) : undefined; - const menuElement = React.useMemo( - () => - document && !isMoving && !isEditing && !isDraggingAnyDocument ? ( - - {can.createChildDocument && ( - - { - ev.preventDefault(); - setIsAddingNewChild(); - setExpanded(); - }} - > - - - - )} - - - ) : undefined, - [ - document, - isMoving, - isEditing, - isDraggingAnyDocument, - can.createChildDocument, - t, - setIsAddingNewChild, - setExpanded, - handleRename, - handleMenuOpen, - handleMenuClose, - ] - ); + const cursorBefore = + isDraggingAnyDocument && collection?.isManualSort && index === 0 ? ( + + ) : undefined; + + const cursorAfter = + isDraggingAnyDocument && collection?.isManualSort ? ( + + ) : undefined; return ( - - - {isDraggingAnyDocument && collection?.isManualSort && index === 0 && ( - - )} - -
- - - -
-
- {isDraggingAnyDocument && collection?.isManualSort && ( - - )} -
- {isAddingNewChild && ( - true} - depth={depth + 1} - ellipsis={false} - label={ - - } - /> - )} {nodeChildren.map((childNode, childIndex) => ( @@ -504,16 +389,8 @@ function InnerDocumentLink( ))} -
+ ); -} - -const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>` - transition: opacity 250ms ease; - opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.1 : 1)}; - pointer-events: ${(props) => (props.$isMoving ? "none" : "inherit")}; -`; - -const DocumentLink = observer(React.forwardRef(InnerDocumentLink)); +}); export default DocumentLink; diff --git a/app/components/Sidebar/components/DocumentRow.tsx b/app/components/Sidebar/components/DocumentRow.tsx new file mode 100644 index 0000000000..0f452a42a2 --- /dev/null +++ b/app/components/Sidebar/components/DocumentRow.tsx @@ -0,0 +1,336 @@ +import type { Location, LocationDescriptor } from "history"; +import { observer } from "mobx-react"; +import { PlusIcon } from "outline-icons"; +import * as React from "react"; +import type { ConnectDragSource } from "react-dnd"; +import { useTranslation } from "react-i18next"; +import type { match } from "react-router"; +import styled from "styled-components"; +import { DocumentValidation } from "@shared/validations"; +import type Document from "~/models/Document"; +import EditableTitle, { type RefHandle } from "~/components/EditableTitle"; +import Fade from "~/components/Fade"; +import NudeButton from "~/components/NudeButton"; +import Tooltip from "~/components/Tooltip"; +import useBoolean from "~/hooks/useBoolean"; +import { ActionContextProvider } from "~/hooks/useActionContext"; +import DropToImport from "./DropToImport"; +import Relative from "./Relative"; +import SidebarLink from "./SidebarLink"; +import type { SidebarContextType } from "./SidebarContext"; +import { useSidebarContext } from "./SidebarContext"; +import type { ActionWithChildren } from "~/types"; + +export type DocumentRowProps = { + /** Document identifier for policy, prefetch and import. */ + documentId: string; + /** Loaded document; used for editing title and active matching. */ + document?: Document; + + /** Navigation target for the row. */ + to: LocationDescriptor; + + /** Indentation depth of the row. */ + depth: number; + /** Applies draft styling around the row. */ + isDraft?: boolean; + /** Scroll this row into view when it becomes the active route. */ + scrollIntoViewIfNeeded?: boolean; + + /** Icon displayed to the left of the label. */ + icon?: React.ReactNode; + /** Displays a small unread badge to the right of the label. */ + unreadBadge?: boolean; + + /** Whether inline title updates are allowed. */ + canEdit?: boolean; + /** Static label content; when provided, it is rendered in preference to `labelText`. */ + label?: React.ReactNode; + /** Label as a text string, for editing. */ + labelText?: string; + /** Submit handler when title updates are allowed. */ + onTitleChange?: (value: string) => Promise; + /** Forwarded ref to the `EditableTitle` instance when it is rendered. */ + editableTitleRef?: React.Ref; + /** Notifies the container when the rendered inline title enters or exits editing mode. */ + onEditingChange?: (editing: boolean) => void; + + /** Whether the row is expanded. */ + expanded: boolean; + /** Whether the row has any descendants (controls whether the disclosure renders). */ + hasChildren: boolean; + /** Called when the disclosure caret or Alt+click toggles expansion. */ + onDisclosureClick: (ev?: React.MouseEvent) => void; + /** Imperative expand, used by the "+" button and ArrowRight keydown. */ + onExpand?: () => void; + /** Imperative collapse, used by ArrowLeft keydown. */ + onCollapse?: () => void; + + /** Drag source ref from the container's drag hook. */ + dragRef?: ConnectDragSource; + /** Whether the row is being dragged. */ + isDragging?: boolean; + /** Whether the row's document is being moved. */ + isMoving?: boolean; + + /** Ref to the outer Relative element; some drop hooks need to read it. */ + parentRef?: React.Ref; + /** Ref for the row's reparent drop target. */ + dropToReparentRef?: React.Ref; + /** Whether the row is an active drop target (visual highlight). */ + isActiveDropTarget?: boolean; + + /** Cursor element rendered above the row. */ + cursorBefore?: React.ReactNode; + /** Cursor element rendered below the row. */ + cursorAfter?: React.ReactNode; + + /** Menu content rendered by the container. */ + menu?: React.ReactNode; + /** Whether the menu's action slot is visible (e.g. while the menu is open). */ + menuOpen?: boolean; + + /** When true, the "+" new-child button is rendered in the menu slot. */ + canCreateChild?: boolean; + /** Submit handler for the inline new-child title input. */ + onCreateChild?: (title: string) => Promise; + /** Depth of the inline new-child SidebarLink. Defaults to `depth + 1`. */ + newChildDepth?: number; + + /** Context menu action for the row. */ + contextAction?: ActionWithChildren; + + /** Optional override for the active-match function. */ + isActiveOverride?: ( + match: match | null, + location: Location<{ sidebarContext?: SidebarContextType }> + ) => boolean; + + /** Content rendered after the row (e.g. a Folder of nested child rows). */ + children?: React.ReactNode; + + /** Called on click intent for prefetching. */ + onClickIntent?: () => void; +}; + +function DocumentRow({ + documentId, + document, + to, + depth, + isDraft, + scrollIntoViewIfNeeded, + icon, + unreadBadge, + label, + canEdit, + labelText, + onTitleChange, + editableTitleRef, + onEditingChange, + expanded, + hasChildren, + onDisclosureClick, + onExpand, + onCollapse, + dragRef, + isDragging, + isMoving, + parentRef, + dropToReparentRef, + isActiveDropTarget, + cursorBefore, + cursorAfter, + menu, + menuOpen, + canCreateChild, + onCreateChild, + newChildDepth, + contextAction, + isActiveOverride, + children, + onClickIntent, +}: DocumentRowProps) { + const { t } = useTranslation(); + const sidebarContext = useSidebarContext(); + const [isEditing, setIsEditingState] = React.useState(false); + const setIsEditing = React.useCallback( + (editing: boolean) => { + setIsEditingState(editing); + onEditingChange?.(editing); + }, + [onEditingChange] + ); + const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] = + useBoolean(); + const newChildTitleRef = React.useRef(null); + + const handleKeyDown = React.useCallback( + (ev: React.KeyboardEvent) => { + if (!hasChildren) { + return; + } + if (ev.key === "ArrowRight" && !expanded) { + onExpand?.(); + } + if (ev.key === "ArrowLeft" && expanded) { + onCollapse?.(); + } + }, + [hasChildren, expanded, onExpand, onCollapse] + ); + + const handleAddChild = React.useCallback( + (ev: React.MouseEvent) => { + ev.preventDefault(); + setIsAddingNewChild(); + onExpand?.(); + }, + [setIsAddingNewChild, onExpand] + ); + + const handleNewChildSubmit = React.useCallback( + async (value: string) => { + if (!onCreateChild) { + return; + } + try { + newChildTitleRef.current?.setIsEditing(false); + await onCreateChild(value); + closeAddingNewChild(); + } catch (_err) { + newChildTitleRef.current?.setIsEditing(true); + } + }, + [onCreateChild, closeAddingNewChild] + ); + + const labelElement = + label ?? + (labelText !== undefined ? ( + undefined)} + isEditing={isEditing} + onEditing={setIsEditing} + canUpdate={!!canEdit} + maxLength={DocumentValidation.maxTitleLength} + ref={editableTitleRef} + /> + ) : null); + + const hasMenuContent = Boolean(menu) || canCreateChild; + const menuVisible = hasMenuContent && !isEditing && !isDragging && !isMoving; + const menuElement = menuVisible ? ( + + {canCreateChild && ( + + + + + + )} + {menu} + + ) : undefined; + + const defaultIsActive = React.useCallback( + ( + m: match | null, + location: Location<{ sidebarContext?: SidebarContextType }> + ) => { + if (sidebarContext !== location.state?.sidebarContext) { + return false; + } + return (document && location.pathname.endsWith(document.urlId)) || !!m; + }, + [sidebarContext, document] + ); + + const sidebarLinkElement = ( + + ); + + const withImport = documentId ? ( + {sidebarLinkElement} + ) : ( + sidebarLinkElement + ); + + return ( + + + {cursorBefore} + + {dropToReparentRef ? ( +
{withImport}
+ ) : ( + withImport + )} +
+ {cursorAfter} +
+ {isAddingNewChild && onCreateChild && ( + true} + depth={newChildDepth ?? depth + 1} + ellipsis={false} + label={ + + } + /> + )} + {children} +
+ ); +} + +const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>` + transition: opacity 250ms ease; + opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.1 : 1)}; + pointer-events: ${(props) => (props.$isMoving ? "none" : "inherit")}; +`; + +export default observer(DocumentRow); diff --git a/app/components/Sidebar/components/SharedWithMeLink.tsx b/app/components/Sidebar/components/SharedWithMeLink.tsx index 9704813936..771cc1ec53 100644 --- a/app/components/Sidebar/components/SharedWithMeLink.tsx +++ b/app/components/Sidebar/components/SharedWithMeLink.tsx @@ -2,12 +2,10 @@ import fractionalIndex from "fractional-index"; import type { Location } from "history"; import { observer } from "mobx-react"; import * as React from "react"; -import styled from "styled-components"; import { IconType, NotificationEventType } from "@shared/types"; import { determineIconType } from "@shared/utils/icon"; import type GroupMembership from "~/models/GroupMembership"; import UserMembership from "~/models/UserMembership"; -import Fade from "~/components/Fade"; import useBoolean from "~/hooks/useBoolean"; import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext"; import useStores from "~/hooks/useStores"; @@ -19,15 +17,14 @@ import { } from "../hooks/useDragAndDrop"; import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon"; import DocumentLink from "./DocumentLink"; +import DocumentRow from "./DocumentRow"; import DropCursor from "./DropCursor"; import Folder from "./Folder"; -import Relative from "./Relative"; import SidebarDisclosureContext, { useSidebarDisclosure, useSidebarDisclosureState, } from "./SidebarDisclosureContext"; import { useSidebarContext, type SidebarContextType } from "./SidebarContext"; -import SidebarLink from "./SidebarLink"; type Props = { membership: UserMembership | GroupMembership; @@ -55,7 +52,6 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) { const { event: disclosureEvent, onDisclosureClick } = useSidebarDisclosureState(); - // Subscribe to recursive expand/collapse events from an ancestor (e.g. GroupLink) useSidebarDisclosure(setExpanded, setCollapsed); React.useEffect(() => { @@ -83,16 +79,16 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) { }, [fetchChildDocuments, isActiveDocument, membership.documentId]); const handleDisclosureClick = React.useCallback( - (ev: React.MouseEvent) => { - ev.preventDefault(); - ev.stopPropagation(); + (ev?: React.MouseEvent) => { + ev?.preventDefault(); + ev?.stopPropagation(); const willExpand = !expanded; if (willExpand) { setExpanded(); } else { setCollapsed(); } - onDisclosureClick(willExpand, ev.altKey); + onDisclosureClick(willExpand, !!ev?.altKey); }, [expanded, setExpanded, setCollapsed, onDisclosureClick] ); @@ -118,107 +114,92 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) { const [reorderProps, dropToReorderRef] = useDropToReorderUserMembership(getIndex); + const isActive = React.useCallback( + (match, location: Location<{ sidebarContext?: SidebarContextType }>) => + !!match && location.state?.sidebarContext === sidebarContext, + [sidebarContext] + ); + const displayChildDocuments = expanded && !isDragging; - if (document) { - const { icon: docIcon } = document; - const label = - determineIconType(docIcon) === IconType.Emoji - ? document.title.replace(docIcon!, "") - : document.titleWithDefault; - const collection = document.collectionId - ? collections.get(document.collectionId) - : undefined; - - const childDocuments = membership.documents ?? []; - - return ( - <> - - -
- 0 && !isDragging - ? expanded - : undefined - } - onDisclosureClick={handleDisclosureClick} - icon={icon} - isActive={( - match, - location: Location<{ sidebarContext?: SidebarContextType }> - ) => - !!match && location.state?.sidebarContext === sidebarContext - } - label={label} - exact={false} - unreadBadge={ - document.unreadNotifications.filter( - (notification) => - notification.event === - NotificationEventType.AddUserToDocument - ).length > 0 - } - $showActions={menuOpen} - menu={ - document && !isDragging ? ( - - - - ) : undefined - } - /> -
-
-
- - - {childDocuments.map((childNode, index) => ( - - ))} - - - {reorderProps.isDragging && ( - - )} - - ); + if (!document) { + return null; } - return null; + const { icon: docIcon } = document; + const label = + determineIconType(docIcon) === IconType.Emoji + ? document.title.replace(docIcon!, "") + : document.titleWithDefault; + const collection = document.collectionId + ? collections.get(document.collectionId) + : undefined; + + const childDocuments = membership.documents ?? []; + const hasChildren = childDocuments.length > 0; + + const unreadBadge = + document.unreadNotifications.filter( + (notification) => + notification.event === NotificationEventType.AddUserToDocument + ).length > 0; + + const menu = !isDragging ? ( + + ) : undefined; + + return ( + + + + {childDocuments.map((childNode, index) => ( + + ))} + + + {reorderProps.isDragging && ( + + )} + + ); } -const Draggable = styled.div<{ $isDragging?: boolean }>` - position: relative; - transition: opacity 250ms ease; - opacity: ${(props) => (props.$isDragging ? 0.1 : 1)}; -`; - export default observer(SharedWithMeLink); diff --git a/app/components/Sidebar/components/StarredLink.tsx b/app/components/Sidebar/components/StarredLink.tsx index 7ff043d841..3dc17385ac 100644 --- a/app/components/Sidebar/components/StarredLink.tsx +++ b/app/components/Sidebar/components/StarredLink.tsx @@ -1,36 +1,45 @@ import fractionalIndex from "fractional-index"; import type { Location } from "history"; import { observer } from "mobx-react"; -import { StarredIcon } from "outline-icons"; import * as React from "react"; import { useEffect, useState } from "react"; -import styled, { useTheme } from "styled-components"; +import { useHistory } from "react-router-dom"; +import styled from "styled-components"; +import { UserPreference } from "@shared/types"; +import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper"; +import type Collection from "~/models/Collection"; +import type Document from "~/models/Document"; import type Star from "~/models/Star"; -import Fade from "~/components/Fade"; +import type { RefHandle } from "~/components/EditableTitle"; import useBoolean from "~/hooks/useBoolean"; +import { useCollectionMenuAction } from "~/hooks/useCollectionMenuAction"; +import useCurrentUser from "~/hooks/useCurrentUser"; +import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction"; import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext"; +import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; +import CollectionMenu from "~/menus/CollectionMenu"; import DocumentMenu from "~/menus/DocumentMenu"; +import { documentEditPath } from "~/utils/routeHelpers"; import { useDragStar, + useDropToChangeCollection, useDropToCreateStar, useDropToReorderStar, } from "../hooks/useDragAndDrop"; import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon"; -import CollectionLink from "./CollectionLink"; +import CollectionLinkChildren from "./CollectionLinkChildren"; +import CollectionRow from "./CollectionRow"; import DocumentLink from "./DocumentLink"; -import SidebarDisclosureContext, { - useSidebarDisclosureState, -} from "./SidebarDisclosureContext"; +import DocumentRow from "./DocumentRow"; import DropCursor from "./DropCursor"; import Folder from "./Folder"; import Relative from "./Relative"; import type { SidebarContextType } from "./SidebarContext"; import SidebarContext, { starredSidebarContext } from "./SidebarContext"; -import SidebarLink from "./SidebarLink"; -import { ActionContextProvider } from "~/hooks/useActionContext"; -import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction"; -import { type ConnectDragSource } from "react-dnd"; +import SidebarDisclosureContext, { + useSidebarDisclosureState, +} from "./SidebarDisclosureContext"; type Props = { star: Star; @@ -38,152 +47,291 @@ type Props = { type StarredDocumentLinkProps = { star: Star; - documentId: string; + document: Document; expanded: boolean; sidebarContext: SidebarContextType; - isDragging: boolean; - handleDisclosureClick: React.MouseEventHandler; + handleDisclosureClick: (ev?: React.MouseEvent) => void; handlePrefetch: () => void; + onExpand: () => void; + onCollapse: () => void; icon: React.ReactNode; - label: React.ReactNode; menuOpen: boolean; handleMenuOpen: () => void; handleMenuClose: () => void; - draggableRef: ConnectDragSource; cursor: React.ReactNode; }; type StarredCollectionLinkProps = { star: Star; - collection: any; + collection: Collection; expanded: boolean; sidebarContext: SidebarContextType; - isDragging: boolean; - handleDisclosureClick: (ev?: React.MouseEvent) => void; - draggableRef: ConnectDragSource; + handleDisclosureClick: (ev?: React.MouseEvent) => void; cursor: React.ReactNode; - displayChildDocuments: boolean; - reorderStarProps: any; + isDraggingAnyStar: boolean; }; const StarredDocumentLink = observer(function StarredDocumentLink({ star, - documentId, + document, expanded, sidebarContext, - isDragging, handleDisclosureClick, handlePrefetch, + onExpand, + onCollapse, icon, - label, menuOpen, handleMenuOpen, handleMenuClose, - draggableRef, cursor, }: StarredDocumentLinkProps) { + const history = useHistory(); + const user = useCurrentUser(); const { collections, documents } = useStores(); + const can = usePolicy(document); + const editableTitleRef = React.useRef(null); + const [{ isDragging }, draggableRef] = useDragStar(star); - const document = documents.get(documentId); - - const documentCollection = document?.collectionId + const documentCollection = document.collectionId ? collections.get(document.collectionId) : undefined; const childDocuments = documentCollection - ? documentCollection.getChildrenForDocument(documentId) + ? documentCollection.getChildrenForDocument(document.id) : []; const hasChildDocuments = childDocuments.length > 0; const displayChildDocuments = expanded && !isDragging; - const contextMenuAction = useDocumentMenuAction({ documentId }); - if (!document) { - return null; - } + const handleRename = React.useCallback(() => { + editableTitleRef.current?.setIsEditing(true); + }, []); + + const handleTitleChange = React.useCallback( + async (value: string) => { + if (!document) { + return; + } + await documents.update({ + id: document.id, + title: value, + }); + }, + [documents, document] + ); + + const handleNewDoc = React.useCallback( + async (input: string) => { + if (!document) { + return; + } + const newDocument = await documents.create( + { + collectionId: documentCollection?.id, + parentDocumentId: document.id, + fullWidth: + document.fullWidth ?? + user.getPreference(UserPreference.FullWidthDocuments), + title: input, + data: ProsemirrorHelper.getEmptyDocument(), + }, + { publish: true } + ); + documentCollection?.addDocument(newDocument, document.id); + history.push({ + pathname: documentEditPath(newDocument), + state: { sidebarContext }, + }); + }, + [documents, document, documentCollection, sidebarContext, user, history] + ); + + const contextMenuAction = useDocumentMenuAction({ + documentId: document.id, + onRename: handleRename, + }); + + const isActive = React.useCallback( + (match, location: Location<{ sidebarContext?: SidebarContextType }>) => { + if (location.state?.sidebarContext !== sidebarContext) { + return false; + } + return ( + !!match || (!!document && location.pathname.endsWith(document.urlId)) + ); + }, + [sidebarContext, document] + ); + + const menu = ( + + ); return ( - - - - ) => !!match && location.state?.sidebarContext === sidebarContext} - label={label} - exact={false} - $showActions={menuOpen} - menu={ - document && !isDragging ? ( - - + + + + + {childDocuments.map((node, index) => ( + - - ) : undefined - } - /> - - - - - {childDocuments.map((node, index) => ( - - ))} - - {cursor} - - - + ))} + + {cursor} + + + + ); }); const StarredCollectionLink = observer(function StarredCollectionLink({ star, collection, + expanded, sidebarContext, - isDragging, handleDisclosureClick, - draggableRef, cursor, - displayChildDocuments, - reorderStarProps, + isDraggingAnyStar, }: StarredCollectionLinkProps) { const { documents } = useStores(); + const history = useHistory(); + const user = useCurrentUser(); + const can = usePolicy(collection.id); + const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); + const editableTitleRef = React.useRef(null); + const [{ isDragging }, draggableRef] = useDragStar(star); + const displayChildDocuments = expanded && !isDragging; + + const handleTitleChange = React.useCallback( + async (name: string) => { + await collection.save({ name }); + }, + [collection] + ); + + const handleExpand = React.useCallback(() => { + if (!displayChildDocuments) { + handleDisclosureClick(); + } + }, [displayChildDocuments, handleDisclosureClick]); + + const parentRef = React.useRef(null); + const [{ isOver, canDrop }, dropRef] = useDropToChangeCollection( + collection, + handleExpand, + parentRef + ); + + const handleRename = React.useCallback(() => { + editableTitleRef.current?.setIsEditing(true); + }, []); + + const handlePrefetch = React.useCallback(() => { + void collection.fetchDocuments(); + }, [collection]); + + const handleNewDoc = React.useCallback( + async (input: string) => { + const newDocument = await documents.create( + { + collectionId: collection.id, + title: input, + fullWidth: user.getPreference(UserPreference.FullWidthDocuments), + data: ProsemirrorHelper.getEmptyDocument(), + }, + { publish: true } + ); + collection?.addDocument(newDocument); + history.push({ + pathname: documentEditPath(newDocument), + state: { sidebarContext }, + }); + }, + [user, sidebarContext, history, collection, documents] + ); + + const contextMenuAction = useCollectionMenuAction({ + collectionId: collection.id, + onRename: handleRename, + }); + + const menu = !isDraggingAnyStar ? ( + + ) : undefined; return ( - - + + onExpand={handleExpand} + onClickIntent={handlePrefetch} + canEdit={can.update} + labelText={collection.name} + onTitleChange={handleTitleChange} + editableTitleRef={editableTitleRef} + contextAction={contextMenuAction} + menu={menu} + menuOpen={menuOpen} + canCreateChild={!isDraggingAnyStar && can.createDocument} + onCreateChild={handleNewDoc} + parentRef={parentRef} + dropRef={dropRef} + isActiveDropTarget={isOver && canDrop} + > + + {cursor} @@ -191,11 +339,11 @@ const StarredCollectionLink = observer(function StarredCollectionLink({ }); function StarredLink({ star }: Props) { - const theme = useTheme(); const { ui, collections, documents } = useStores(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const { documentId, collectionId } = star; const collection = collectionId ? collections.get(collectionId) : undefined; + const document = documentId ? documents.get(documentId) : undefined; const locationSidebarContext = useLocationSidebarContext(); const sidebarContext = starredSidebarContext( star.documentId ?? star.collectionId ?? "" @@ -250,6 +398,14 @@ function StarredLink({ star }: Props) { [onDisclosureClick] ); + const handleExpand = React.useCallback(() => { + setExpanded(true); + }, []); + + const handleCollapse = React.useCallback(() => { + setExpanded(false); + }, []); + const handlePrefetch = React.useCallback(() => { if (documentId) { void documents.prefetchDocument(documentId); @@ -265,16 +421,10 @@ function StarredLink({ star }: Props) { const next = star?.next(); return fractionalIndex(star?.index || null, next?.index || null); }; - const { label, icon } = useSidebarLabelAndIcon( - star, - - ); - const [{ isDragging }, draggableRef] = useDragStar(star); + const { icon } = useSidebarLabelAndIcon(star); const [reorderStarProps, dropToReorderRef] = useDropToReorderStar(getIndex); const [createStarProps, dropToStarRef] = useDropToCreateStar(getIndex); - const displayChildDocuments = expanded && !isDragging; - const cursor = ( <> {reorderStarProps.isDragging && ( @@ -292,23 +442,22 @@ function StarredLink({ star }: Props) { ); - if (documentId) { + if (document) { return ( @@ -323,12 +472,9 @@ function StarredLink({ star }: Props) { collection={collection} expanded={expanded} sidebarContext={sidebarContext} - isDragging={isDragging} handleDisclosureClick={handleDisclosureClick} - draggableRef={draggableRef} cursor={cursor} - displayChildDocuments={displayChildDocuments} - reorderStarProps={reorderStarProps} + isDraggingAnyStar={reorderStarProps.isDragging} /> ); diff --git a/app/components/Sidebar/hooks/useDragAndDrop.tsx b/app/components/Sidebar/hooks/useDragAndDrop.tsx index 61ff400f2e..558132a05a 100644 --- a/app/components/Sidebar/hooks/useDragAndDrop.tsx +++ b/app/components/Sidebar/hooks/useDragAndDrop.tsx @@ -1,12 +1,10 @@ import fractionalIndex from "fractional-index"; -import { StarredIcon } from "outline-icons"; import * as React from "react"; import type { ConnectDragSource } from "react-dnd"; import { useDrag, useDrop } from "react-dnd"; import { getEmptyImage } from "react-dnd-html5-backend"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { useTheme } from "styled-components"; import Icon from "@shared/components/Icon"; import type { NavigationNode } from "@shared/types"; import type Collection from "~/models/Collection"; @@ -68,11 +66,8 @@ export function useDragStar( star: Star ): [{ isDragging: boolean }, ConnectDragSource] { const id = star.id; - const theme = useTheme(); - const { label: title, icon } = useSidebarLabelAndIcon( - star, - - ); + const { label: title, icon } = useSidebarLabelAndIcon(star); + const [{ isDragging }, draggableRef, preview] = useDrag({ type: "star", item: () => ({ id, title, icon }), @@ -495,21 +490,12 @@ export function useDragMembership( const id = membership.id; const { label: title, icon } = useSidebarLabelAndIcon(membership); - const [{ isDragging }, draggableRef, preview] = useDrag< - DragObject, - Promise, - { isDragging: boolean } - >({ + const [{ isDragging }, draggableRef, preview] = useDrag({ type: membership instanceof UserMembership ? "userMembership" : "groupMembership", - item: () => - ({ - id, - title, - icon, - }) as DragObject, + item: () => ({ id, title, icon }), collect: (monitor) => ({ isDragging: !!monitor.isDragging(), }), diff --git a/app/components/Sidebar/hooks/useSidebarLabelAndIcon.tsx b/app/components/Sidebar/hooks/useSidebarLabelAndIcon.tsx index e95884ce8f..4d5e4edf41 100644 --- a/app/components/Sidebar/hooks/useSidebarLabelAndIcon.tsx +++ b/app/components/Sidebar/hooks/useSidebarLabelAndIcon.tsx @@ -1,4 +1,4 @@ -import { DocumentIcon } from "outline-icons"; +import { DocumentIcon, QuestionMarkIcon } from "outline-icons"; import * as React from "react"; import Icon from "@shared/components/Icon"; import CollectionIcon from "~/components/Icons/CollectionIcon"; @@ -9,12 +9,12 @@ interface SidebarItem { collectionId?: string; } -export function useSidebarLabelAndIcon( - { documentId, collectionId }: SidebarItem, - defaultIcon?: React.ReactNode -) { +export function useSidebarLabelAndIcon({ + documentId, + collectionId, +}: SidebarItem) { const { collections, documents } = useStores(); - const icon = defaultIcon ?? ; + const icon = ; if (documentId) { const document = documents.get(documentId); @@ -28,7 +28,7 @@ export function useSidebarLabelAndIcon( color={document.color ?? undefined} /> ) : ( - icon + ), }; } diff --git a/app/hooks/useImportDocument.ts b/app/hooks/useImportDocument.ts index ac30da9f03..e07e9fbcd1 100644 --- a/app/hooks/useImportDocument.ts +++ b/app/hooks/useImportDocument.ts @@ -3,6 +3,7 @@ import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { toast } from "sonner"; +import { useSidebarContext } from "~/components/Sidebar/components/SidebarContext"; import useStores from "~/hooks/useStores"; import { documentPath } from "~/utils/routeHelpers"; @@ -16,6 +17,7 @@ export default function useImportDocument( isImporting: boolean; } { const { documents } = useStores(); + const sidebarContext = useSidebarContext(); const [isImporting, setImporting] = useState(false); const { t } = useTranslation(); const history = useHistory(); @@ -53,7 +55,10 @@ export default function useImportDocument( }); if (redirect) { - history.push(documentPath(doc)); + history.push({ + pathname: documentPath(doc), + state: { sidebarContext }, + }); } } catch (err) { toast.error(err.message); @@ -68,7 +73,7 @@ export default function useImportDocument( importingLock = false; } }, - [t, documents, history, collectionId, documentId] + [t, documents, history, collectionId, sidebarContext, documentId] ); return { diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 587ae04ca3..447190e073 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -281,7 +281,7 @@ class DocumentScene extends React.Component { if (location.pathname.endsWith("history")) { this.props.history.push({ - pathname: document.url, + pathname: document.path, state: { sidebarContext: this.props.location.state?.sidebarContext }, }); } else { diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 3bf6d14226..ffc577ddab 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -500,9 +500,9 @@ "Expand sidebar": "Expand sidebar", "Collapse sidebar": "Collapse sidebar", "Archived collections": "Archived collections", + "Empty": "Empty", "New doc": "New doc", "New nested document": "New nested document", - "Empty": "Empty", "No collections": "No collections", "Collapse": "Collapse", "Expand": "Expand",