fix: Add ability to collapse and expand collections that are not active (#3102)

* fix: add disclosure and transition

* fix: keep collections expanded

* fix: tune transition and collapsing conditions

* fix: collectionIcon expanded props is no longer driven by expanded state

* fix: sync issue

* fix: managing state together

* fix: remove comment

* fix: simplify expanded state

* fix: remove extra state

* fix: remove animation and retain expanded state

* fix: remove isCollectionDropped

* fix: don't use ref

* review suggestions

* fix many functional and design issues

* don't render every single document in the sidebar, just ones that the user has seen before

* chore: Sidebar refinement (#3154)

* stash

* wip: More sidebar tweaks

* Simplify draft bubble

* disclosure refactor

* wip wip

* lint

* tweak menu position

* Use document emoji for starred docs where available

* feat: Trigger cmd+k from sidebar (#3149)

* feat: Trigger cmd+k from sidebar

* Add hint when opening command bar from sidebar

* fix: Clicking internal links in shared documents sometimes reroutes to Login

* fix: Spacing issues on connected slack channels list

* Merge

* fix: Do not prefetch JS bundles on public share links

* fix: Buttons show on collection empty state when user does not have permission to edit

* fix: the hover area for the "collections" subheading was being obfuscated by the initial collection drop cursor

* fix: top-align disclosures

* fix: Disclosure color PR feedback
fix: Starred no longer draggable

* fix: Overflow on sidebar button

* fix: Scrollbar in sidebar when command menu is open

* Minor alignment issues, clarify back in settings sidebar

* fix: Fade component causes SidebarButton missizing

Co-authored-by: Nan Yu <thenanyu@gmail.com>

Co-authored-by: Tom Moor <tom.moor@gmail.com>
Co-authored-by: Nan Yu <thenanyu@gmail.com>
This commit is contained in:
Saumya Pandey
2022-02-24 10:56:38 +05:30
committed by GitHub
parent ce33a4b219
commit 4c95674ef0
30 changed files with 694 additions and 522 deletions

View File

@@ -1,7 +1,7 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import * as React from "react";
import { useDrop, useDrag } from "react-dnd";
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import styled from "styled-components";
@@ -70,6 +70,13 @@ function CollectionLink({
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 = policies.abilities(collection.id);
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
@@ -118,7 +125,7 @@ function CollectionLink({
// Drop to reorder document
const [{ isOverReorder }, dropToReorder] = useDrop({
accept: "document",
drop: async (item: DragObject) => {
drop: (item: DragObject) => {
if (!collection) {
return;
}
@@ -131,11 +138,11 @@ function CollectionLink({
// Drop to reorder collection
const [
{ isCollectionDropping, isDraggingAnotherCollection },
{ isCollectionDropping, isDraggingAnyCollection },
dropToReorderCollection,
] = useDrop({
accept: "collection",
drop: async (item: DragObject) => {
drop: (item: DragObject) => {
collections.move(
item.id,
fractionalIndex(collection.index, belowCollectionIndex)
@@ -147,9 +154,9 @@ function CollectionLink({
(!belowCollection || item.id !== belowCollection.id)
);
},
collect: (monitor) => ({
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
isCollectionDropping: monitor.isOver(),
isDraggingAnotherCollection: monitor.canDrop(),
isDraggingAnyCollection: monitor.getItemType() === "collection",
}),
});
@@ -194,8 +201,7 @@ function CollectionLink({
collection.sort,
]);
const isDraggingAnyCollection =
isDraggingAnotherCollection || isCollectionDragging;
const displayDocumentLinks = expanded && !isCollectionDragging;
React.useEffect(() => {
// If we're viewing a starred document through the starred menu then don't
@@ -204,21 +210,14 @@ function CollectionLink({
return;
}
if (isDraggingAnyCollection) {
setExpanded(false);
} else {
setExpanded(collection.id === ui.activeCollectionId);
if (collection.id === ui.activeCollectionId) {
setExpanded(true);
}
}, [isDraggingAnyCollection, collection.id, ui.activeCollectionId, search]);
}, [collection.id, ui.activeCollectionId, search]);
return (
<>
<div
ref={drop}
style={{
position: "relative",
}}
>
<Relative ref={drop}>
<Draggable
key={collection.id}
ref={dragToReorderCollection}
@@ -228,8 +227,16 @@ function CollectionLink({
<DropToImport collectionId={collection.id}>
<SidebarLink
to={collection.url}
expanded={displayDocumentLinks}
onDisclosureClick={(event) => {
event.preventDefault();
setExpanded((prev) => !prev);
}}
icon={
<CollectionIcon collection={collection} expanded={expanded} />
<CollectionIcon
collection={collection}
expanded={displayDocumentLinks}
/>
}
showActions={menuOpen}
isActiveDrop={isOver && canDrop}
@@ -242,12 +249,13 @@ function CollectionLink({
/>
}
exact={false}
depth={0.5}
depth={0}
menu={
!isEditing && (
!isEditing &&
!isDraggingAnyCollection && (
<>
{can.update && (
<CollectionSortMenuWithMargin
{can.update && displayDocumentLinks && (
<CollectionSortMenu
collection={collection}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
@@ -264,8 +272,31 @@ function CollectionLink({
/>
</DropToImport>
</Draggable>
{expanded && manualSort && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
</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}
canUpdate={canUpdate}
isDraft={node.isDraft}
depth={2}
index={index}
/>
))}
</Folder>
)}
{isDraggingAnyCollection && (
<DropCursor
@@ -273,21 +304,8 @@ function CollectionLink({
innerRef={dropToReorderCollection}
/>
)}
</div>
{expanded &&
collectionDocuments.map((node, index) => (
<DocumentLink
key={node.id}
node={node}
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
isDraft={node.isDraft}
depth={2}
index={index}
/>
))}
</Relative>
<Modal
title={t("Move document")}
onRequestClose={handlePermissionClose}
@@ -306,13 +324,17 @@ function CollectionLink({
);
}
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")};
`;
const CollectionSortMenuWithMargin = styled(CollectionSortMenu)`
margin-right: 4px;
`;
export default observer(CollectionLink);

View File

@@ -1,6 +1,5 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
@@ -13,9 +12,10 @@ import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import CollectionLink from "./CollectionLink";
import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import SidebarAction from "./SidebarAction";
import SidebarLink, { DragObject } from "./SidebarLink";
import { DragObject } from "./SidebarLink";
function Collections() {
const [isFetching, setFetching] = React.useState(false);
@@ -52,7 +52,10 @@ function Collections() {
load();
}, [collections, isFetching, showToast, fetchError, t]);
const [{ isCollectionDropping }, dropToReorderCollection] = useDrop({
const [
{ isCollectionDropping, isDraggingAnyCollection },
dropToReorderCollection,
] = useDrop({
accept: "collection",
drop: async (item: DragObject) => {
collections.move(
@@ -65,16 +68,19 @@ function Collections() {
},
collect: (monitor) => ({
isCollectionDropping: monitor.isOver(),
isDraggingAnyCollection: monitor.getItemType() === "collection",
}),
});
const content = (
<>
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
/>
{isDraggingAnyCollection && (
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
/>
)}
{orderedCollections.map((collection: Collection, index: number) => (
<CollectionLink
key={collection.id}
@@ -85,17 +91,14 @@ function Collections() {
belowCollection={orderedCollections[index + 1]}
/>
))}
<SidebarAction action={createCollection} depth={0.5} />
<SidebarAction action={createCollection} depth={0} />
</>
);
if (!collections.isLoaded || fetchError) {
return (
<Flex column>
<SidebarLink
label={t("Collections")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
/>
<Header>{t("Collections")}</Header>
<PlaceholderCollections />
</Flex>
);
@@ -103,19 +106,18 @@ function Collections() {
return (
<Flex column>
<SidebarLink
onClick={() => setExpanded((prev) => !prev)}
label={t("Collections")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
/>
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
<Header onClick={() => setExpanded((prev) => !prev)} expanded={expanded}>
{t("Collections")}
</Header>
{expanded && (
<Relative>{isPreloaded ? content : <Fade>{content}</Fade>}</Relative>
)}
</Flex>
);
}
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
transition: transform 100ms ease, fill 50ms !important;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
const Relative = styled.div`
position: relative;
`;
export default observer(Collections);

View File

@@ -1,12 +0,0 @@
import { CollapsedIcon } from "outline-icons";
import styled from "styled-components";
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
transition: transform 100ms ease, fill 50ms !important;
position: absolute;
left: -24px;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default Disclosure;

View File

@@ -0,0 +1,54 @@
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import styled, { css } from "styled-components";
import NudeButton from "~/components/NudeButton";
type Props = {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
expanded: boolean;
root?: boolean;
};
function Disclosure({ onClick, root, expanded, ...rest }: Props) {
return (
<Button size={20} onClick={onClick} $root={root} {...rest}>
<StyledCollapsedIcon expanded={expanded} size={20} color="currentColor" />
</Button>
);
}
const Button = styled(NudeButton)<{ $root?: boolean }>`
position: absolute;
left: -24px;
flex-shrink: 0;
color: ${(props) => props.theme.textSecondary};
&:hover {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.sidebarControlHoverBackground};
}
${(props) =>
props.$root &&
css`
opacity: 0;
left: -16px;
&:hover {
opacity: 1;
background: none;
}
`}
`;
const StyledCollapsedIcon = styled(CollapsedIcon)<{
expanded?: boolean;
}>`
transition: opacity 100ms ease, transform 100ms ease, fill 50ms !important;
${(props) => !props.expanded && "transform: rotate(-90deg);"};
`;
// Enables identifying this component within styled components
const StyledDisclosure = styled(Disclosure)``;
export default StyledDisclosure;

View File

@@ -16,7 +16,6 @@ import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import { NavigationNode } from "~/types";
import { newDocumentPath } from "~/utils/routeHelpers";
import Disclosure from "./Disclosure";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle from "./EditableTitle";
@@ -81,7 +80,9 @@ function DocumentLink(
isActiveDocument)
);
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
const [expanded, setExpanded] = React.useState(showChildren);
const [openedOnce, setOpenedOnce] = React.useState(expanded);
React.useEffect(() => {
if (showChildren) {
@@ -89,6 +90,12 @@ 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
React.useEffect(() => {
@@ -98,7 +105,7 @@ function DocumentLink(
}, [expanded, hasChildDocuments]);
const handleDisclosureClick = React.useCallback(
(ev: React.SyntheticEvent) => {
(ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
@@ -270,6 +277,8 @@ function DocumentLink(
t("Untitled");
const can = policies.abilities(node.id);
const isExpanded = expanded && !isDragging;
const hasChildren = nodeChildren.length > 0;
return (
<>
@@ -283,6 +292,8 @@ function DocumentLink(
<div ref={dropToReparent}>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
expanded={hasChildren ? isExpanded : undefined}
onDisclosureClick={handleDisclosureClick}
onMouseEnter={handleMouseEnter}
to={{
pathname: node.url,
@@ -291,21 +302,13 @@ function DocumentLink(
},
}}
label={
<>
{hasChildDocuments && (
<Disclosure
expanded={expanded && !isDragging}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={title}
onSubmit={handleTitleChange}
onEditing={handleTitleEditing}
canUpdate={canUpdate}
maxLength={MAX_TITLE_LENGTH}
/>
</>
<EditableTitle
title={title}
onSubmit={handleTitleChange}
onEditing={handleTitleEditing}
canUpdate={canUpdate}
maxLength={MAX_TITLE_LENGTH}
/>
}
isActive={(match, location) =>
!!match && location.search !== "?starred"
@@ -351,26 +354,32 @@ function DocumentLink(
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</Relative>
{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}
/>
))}
{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>
)}
</>
);
}
const Folder = styled.div<{ $open?: boolean }>`
display: ${(props) => (props.$open ? "block" : "none")};
`;
const Relative = styled.div`
position: relative;
`;

View File

@@ -23,7 +23,7 @@ const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>`
width: 100%;
height: 14px;
background: transparent;
${(props) => (props.position === "top" ? "top: 25px;" : "bottom: -7px;")}
${(props) => (props.position === "top" ? "top: -7px;" : "bottom: -7px;")}
::after {
background: ${(props) => props.theme.slateDark};

View File

@@ -1,14 +0,0 @@
import styled from "styled-components";
import Flex from "~/components/Flex";
const Header = styled(Flex)`
font-size: 11px;
font-weight: 600;
user-select: none;
text-transform: uppercase;
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 4px 12px;
`;
export default Header;

View File

@@ -0,0 +1,64 @@
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
type Props = {
onClick?: React.MouseEventHandler;
expanded?: boolean;
children: React.ReactNode;
};
export function Header({ onClick, expanded, children }: Props) {
return (
<H3>
<Button onClick={onClick} disabled={!onClick}>
{children}
{onClick && (
<Disclosure expanded={expanded} color="currentColor" size={20} />
)}
</Button>
</H3>
);
}
const Button = styled.button`
display: inline-flex;
align-items: center;
font-size: 13px;
font-weight: 600;
user-select: none;
color: ${(props) => props.theme.textTertiary};
letter-spacing: 0.03em;
margin: 0;
padding: 4px 2px 4px 12px;
height: 22px;
border: 0;
background: none;
border-radius: 4px;
-webkit-appearance: none;
transition: all 100ms ease;
&:not(:disabled):hover,
&:not(:disabled):active {
color: ${(props) => props.theme.textSecondary};
cursor: pointer;
}
`;
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
transition: opacity 100ms ease, transform 100ms ease, fill 50ms !important;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
opacity: 0;
`;
const H3 = styled.h3`
margin: 0;
&:hover {
${Disclosure} {
opacity: 1;
}
}
`;
export default Header;

View File

@@ -0,0 +1,81 @@
import { ExpandedIcon, MoreIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from "~/components/Flex";
type Props = {
title: React.ReactNode;
image: React.ReactNode;
minHeight?: number;
rounded?: boolean;
showDisclosure?: boolean;
showMoreMenu?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
};
const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
(
{
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
...rest
}: Props,
ref
) => (
<Wrapper
justify="space-between"
align="center"
as="button"
minHeight={minHeight}
{...rest}
ref={ref}
>
<Title gap={4} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon color="currentColor" />}
{showMoreMenu && <MoreIcon color="currentColor" />}
</Wrapper>
)
);
const Title = styled(Flex)`
color: ${(props) => props.theme.text};
flex-shrink: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
`;
const Wrapper = styled(Flex)<{ minHeight: number }>`
padding: 8px 4px;
font-size: 15px;
font-weight: 500;
border-radius: 4px;
margin: 8px;
color: ${(props) => props.theme.textTertiary};
border: 0;
background: none;
flex-shrink: 0;
min-height: ${(props) => props.minHeight}px;
-webkit-appearance: none;
text-decoration: none;
text-align: left;
overflow: hidden;
user-select: none;
cursor: pointer;
&:active,
&:hover {
color: ${(props) => props.theme.sidebarText};
transition: background 100ms ease-in-out;
background: ${(props) => props.theme.sidebarActiveBackground};
}
`;
export default SidebarButton;

View File

@@ -1,10 +1,10 @@
import { transparentize } from "polished";
import * as React from "react";
import styled, { useTheme, css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "~/components/EventBoundary";
import NudeButton from "~/components/NudeButton";
import { NavigationNode } from "~/types";
import Disclosure from "./Disclosure";
import NavLink, { Props as NavLinkProps } from "./NavLink";
export type DragObject = NavigationNode & {
@@ -19,11 +19,14 @@ type Props = Omit<NavLinkProps, "to"> & {
innerRef?: (arg0: HTMLElement | null | undefined) => void;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
onDisclosureClick?: React.MouseEventHandler<HTMLButtonElement>;
icon?: React.ReactNode;
label?: React.ReactNode;
menu?: React.ReactNode;
showActions?: boolean;
active?: boolean;
/* If set, a disclosure will be rendered to the left of any icon */
expanded?: boolean;
isActiveDrop?: boolean;
isDraft?: boolean;
depth?: number;
@@ -50,6 +53,8 @@ function SidebarLink(
href,
depth,
className,
expanded,
onDisclosureClick,
...rest
}: Props,
ref: React.RefObject<HTMLAnchorElement>
@@ -66,10 +71,10 @@ function SidebarLink(
() => ({
fontWeight: 600,
color: theme.text,
background: theme.sidebarItemBackground,
background: theme.sidebarActiveBackground,
...style,
}),
[theme, style]
[theme.text, theme.sidebarActiveBackground, style]
);
return (
@@ -90,14 +95,34 @@ function SidebarLink(
ref={ref}
{...rest}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
<Content>
{expanded !== undefined && (
<Disclosure
expanded={expanded}
onClick={onDisclosureClick}
root={depth === 0}
/>
)}
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
</Content>
</Link>
{menu && <Actions showActions={showActions}>{menu}</Actions>}
</>
);
}
const Content = styled.span`
display: flex;
align-items: start;
position: relative;
width: 100%;
${Disclosure} {
margin-top: 2px;
}
`;
// accounts for whitespace around icon
export const IconWrapper = styled.span`
margin-left: -4px;
@@ -112,6 +137,7 @@ const Actions = styled(EventBoundary)<{ showActions?: boolean }>`
position: absolute;
top: 4px;
right: 4px;
gap: 4px;
color: ${(props) => props.theme.textTertiary};
transition: opacity 50ms;
@@ -158,10 +184,8 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
transition: fill 50ms;
}
&:focus {
color: ${(props) => props.theme.text};
background: ${(props) =>
transparentize("0.25", props.theme.sidebarItemBackground)};
&:hover svg {
display: inline;
}
& + ${Actions} {
@@ -170,16 +194,9 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
}
}
&:focus + ${Actions} {
${NudeButton} {
background: ${(props) =>
transparentize("0.25", props.theme.sidebarItemBackground)};
}
}
&[aria-current="page"] + ${Actions} {
${NudeButton} {
background: ${(props) => props.theme.sidebarItemBackground};
background: ${(props) => props.theme.sidebarActiveBackground};
}
}
@@ -202,6 +219,12 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
props.$isActiveDrop ? props.theme.white : props.theme.text};
}
}
&:hover {
${Disclosure} {
opacity: 1;
}
}
`;
const Label = styled.div`
@@ -209,6 +232,7 @@ const Label = styled.div`
width: 100%;
max-height: 4.8em;
line-height: 1.6;
* {
unicode-bidi: plaintext;
}

View File

@@ -1,6 +1,5 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
@@ -10,8 +9,8 @@ import Flex from "~/components/Flex";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import Section from "./Section";
import SidebarLink from "./SidebarLink";
import StarredLink from "./StarredLink";
@@ -119,71 +118,64 @@ function Starred() {
}),
});
const content = 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}
depth={2}
/>
) : null;
});
if (!stars.orderedData.length) {
return null;
}
return (
<Section>
<Flex column>
<SidebarLink
onClick={handleExpandClick}
label={t("Starred")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
/>
{expanded && (
<>
<DropCursor
isActiveDrop={isOverReorder}
innerRef={dropToReorder}
position="top"
<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}
depth={0}
/>
) : null;
})}
{show === "More" && !isFetching && (
<SidebarLink
onClick={handleShowMore}
label={`${t("Show more")}`}
depth={0}
/>
{content}
{show === "More" && !isFetching && (
<SidebarLink
onClick={handleShowMore}
label={`${t("Show more")}`}
depth={2}
/>
)}
{show === "Less" && !isFetching && (
<SidebarLink
onClick={handleShowLess}
label={`${t("Show less")}`}
depth={2}
/>
)}
{(isFetching || fetchError) && !stars.orderedData.length && (
<Flex column>
<PlaceholderCollections />
</Flex>
)}
</>
)}
</Flex>
</Section>
)}
{show === "Less" && !isFetching && (
<SidebarLink
onClick={handleShowLess}
label={`${t("Show less")}`}
depth={0}
/>
)}
{(isFetching || fetchError) && !stars.orderedData.length && (
<Flex column>
<PlaceholderCollections />
</Flex>
)}
</Relative>
)}
</Flex>
);
}
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
transition: transform 100ms ease, fill 50ms !important;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
const Relative = styled.div`
position: relative;
`;
export default observer(Starred);

View File

@@ -1,19 +1,18 @@
import fractionalIndex from "fractional-index";
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 { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_TITLE_LENGTH } from "@shared/constants";
import styled, { useTheme } from "styled-components";
import parseTitle from "@shared/utils/parseTitle";
import Star from "~/models/Star";
import EmojiIcon from "~/components/EmojiIcon";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import Disclosure from "./Disclosure";
import DropCursor from "./DropCursor";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
type Props = {
@@ -27,24 +26,22 @@ type Props = {
function StarredLink({
depth,
title,
to,
documentId,
title,
collectionId,
star,
}: Props) {
const { t } = useTranslation();
const { collections, documents, policies } = useStores();
const theme = useTheme();
const { collections, documents } = useStores();
const collection = collections.get(collectionId);
const document = documents.get(documentId);
const [expanded, setExpanded] = useState(false);
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const canUpdate = policies.abilities(documentId).update;
const childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];
const hasChildDocuments = childDocuments.length > 0;
const [isEditing, setIsEditing] = React.useState(false);
useEffect(() => {
async function load() {
@@ -57,7 +54,7 @@ function StarredLink({
}, [collection, collectionId, collections, document, documentId, documents]);
const handleDisclosureClick = React.useCallback(
(ev: React.MouseEvent<SVGElement>) => {
(ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((prevExpanded) => !prevExpanded);
@@ -65,29 +62,6 @@ function StarredLink({
[]
);
const handleTitleChange = React.useCallback(
async (title: string) => {
if (!document) {
return;
}
await documents.update(
{
id: document.id,
text: document.text,
title,
},
{
lastRevision: document.revision,
}
);
},
[documents, document]
);
const handleTitleEditing = React.useCallback((isEditing: boolean) => {
setIsEditing(isEditing);
}, []);
// Draggable
const [{ isDragging }, drag] = useDrag({
type: "star",
@@ -96,7 +70,7 @@ function StarredLink({
isDragging: !!monitor.isDragging(),
}),
canDrag: () => {
return depth === 2;
return depth === 0;
},
});
@@ -116,36 +90,34 @@ function StarredLink({
}),
});
const { emoji } = parseTitle(title);
const label = emoji ? title.replace(emoji, "") : title;
return (
<>
<Draggable key={documentId} ref={drag} $isDragging={isDragging}>
<SidebarLink
depth={depth}
expanded={hasChildDocuments ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
to={`${to}?starred`}
icon={
depth === 0 ? (
emoji ? (
<EmojiIcon emoji={emoji} />
) : (
<StarredIcon color={theme.yellow} />
)
) : undefined
}
isActive={(match, location) =>
!!match && location.search === "?starred"
}
label={
<>
{hasChildDocuments && (
<Disclosure
expanded={expanded}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={title || t("Untitled")}
onSubmit={handleTitleChange}
onEditing={handleTitleEditing}
canUpdate={canUpdate}
maxLength={MAX_TITLE_LENGTH}
/>
</>
}
label={depth === 0 ? label : title}
exact={false}
showActions={menuOpen}
menu={
document && !isEditing ? (
document ? (
<Fade>
<DocumentMenu
document={document}
@@ -164,7 +136,7 @@ function StarredLink({
childDocuments.map((childDocument) => (
<ObserveredStarredLink
key={childDocument.id}
depth={depth + 1}
depth={depth === 0 ? 2 : depth + 1}
title={childDocument.title}
to={childDocument.url}
documentId={childDocument.id}

View File

@@ -1,92 +0,0 @@
import { observer } from "mobx-react";
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from "~/components/Flex";
import TeamLogo from "~/components/TeamLogo";
type Props = {
teamName: string;
subheading: React.ReactNode;
showDisclosure?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
logoUrl: string;
};
const TeamButton = React.forwardRef<HTMLButtonElement, Props>(
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
<Wrapper>
<Header ref={ref} {...rest}>
<TeamLogo
alt={`${teamName} logo`}
src={logoUrl}
width={38}
height={38}
/>
<Flex align="flex-start" column>
<TeamName>
{teamName} {showDisclosure && <Disclosure color="currentColor" />}
</TeamName>
<Subheading>{subheading}</Subheading>
</Flex>
</Header>
</Wrapper>
)
);
const Disclosure = styled(ExpandedIcon)`
position: absolute;
right: 0;
top: 0;
`;
const Subheading = styled.div`
padding-left: 10px;
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
white-space: nowrap;
color: ${(props) => props.theme.sidebarText};
`;
const TeamName = styled.div`
position: relative;
padding-left: 10px;
padding-right: 24px;
font-weight: 600;
color: ${(props) => props.theme.text};
white-space: nowrap;
text-decoration: none;
font-size: 16px;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
`;
const Wrapper = styled.div`
flex-shrink: 0;
overflow: hidden;
`;
const Header = styled.button`
display: flex;
align-items: center;
background: none;
line-height: inherit;
border: 0;
padding: 8px;
margin: 8px;
border-radius: 4px;
cursor: pointer;
width: calc(100% - 16px);
&:active,
&:hover {
transition: background 100ms ease-in-out;
background: ${(props) => props.theme.sidebarItemBackground};
}
`;
export default observer(TeamButton);