fix: Show starred docs in sidebar (#2317)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Saumya Pandey
2021-08-23 13:07:28 +05:30
committed by GitHub
parent a50471959b
commit f6d889f759
18 changed files with 346 additions and 113 deletions

View File

@ -2,12 +2,11 @@
import { observer } from "mobx-react";
import {
EditIcon,
SearchIcon,
ShapesIcon,
HomeIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShapesIcon,
StarredIcon,
} from "outline-icons";
import * as React from "react";
import { DndProvider } from "react-dnd";
@ -25,6 +24,7 @@ import ArchiveLink from "./components/ArchiveLink";
import Collections from "./components/Collections";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import Starred from "./components/Starred";
import TeamButton from "./components/TeamButton";
import TrashLink from "./components/TrashLink";
import useCurrentTeam from "hooks/useCurrentTeam";
@ -109,12 +109,6 @@ function MainSidebar() {
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label={t("Starred")}
/>
{can.createDocument && (
<SidebarLink
to="/drafts"
@ -135,6 +129,7 @@ function MainSidebar() {
/>
)}
</Section>
<Starred />
<Section auto>
<Collections
onCreateCollection={handleCreateCollectionModalOpen}

View File

@ -159,6 +159,7 @@ function CollectionLink({
/>
}
exact={false}
depth={0.5}
menu={
<>
{can.update && (
@ -198,7 +199,7 @@ function CollectionLink({
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
depth={1.5}
depth={2}
index={index}
/>
))}

View File

@ -1,16 +1,16 @@
// @flow
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import { PlusIcon, CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Fade from "components/Fade";
import Flex from "components/Flex";
import useStores from "../../../hooks/useStores";
import CollectionLink from "./CollectionLink";
import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import SidebarLink from "./SidebarLink";
import useCurrentTeam from "hooks/useCurrentTeam";
@ -25,6 +25,7 @@ function Collections({ onCreateCollection }: Props) {
const [fetchError, setFetchError] = React.useState();
const { ui, policies, documents, collections } = useStores();
const { showToast } = useToasts();
const [expanded, setExpanded] = React.useState(true);
const isPreloaded: boolean = !!collections.orderedData.length;
const { t } = useTranslation();
const team = useCurrentTeam();
@ -99,6 +100,7 @@ function Collections({ onCreateCollection }: Props) {
icon={<PlusIcon color="currentColor" />}
label={`${t("New collection")}`}
exact
depth={0.5}
/>
)}
</>
@ -107,7 +109,11 @@ function Collections({ onCreateCollection }: Props) {
if (!collections.isLoaded || fetchError) {
return (
<Flex column>
<Header>{t("Collections")}</Header>
<SidebarLink
label={t("Collections")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
disabled
/>
<PlaceholderCollections />
</Flex>
);
@ -115,10 +121,19 @@ function Collections({ onCreateCollection }: Props) {
return (
<Flex column>
<Header>{t("Collections")}</Header>
{isPreloaded ? content : <Fade>{content}</Fade>}
<SidebarLink
onClick={() => setExpanded((prev) => !prev)}
label={t("Collections")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
/>
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
</Flex>
);
}
const Disclosure = styled(CollapsedIcon)`
transition: transform 100ms ease, fill 50ms !important;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default observer(Collections);

View File

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

View File

@ -1,6 +1,5 @@
// @flow
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useDrag, useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
@ -8,6 +7,7 @@ import styled from "styled-components";
import Collection from "models/Collection";
import Document from "models/Document";
import Fade from "components/Fade";
import Disclosure from "./Disclosure";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle from "./EditableTitle";
@ -210,7 +210,7 @@ function DocumentLink(
return (
<>
<div style={{ position: "relative" }} onDragLeave={resetHoverExpanding}>
<Relative onDragLeave={resetHoverExpanding}>
<Draggable
key={node.id}
ref={drag}
@ -244,6 +244,7 @@ function DocumentLink(
depth={depth}
exact={false}
showActions={menuOpen}
scrollIntoViewIfNeeded={!document?.isStarred}
ref={ref}
menu={
document && !isMoving ? (
@ -263,7 +264,7 @@ function DocumentLink(
{manualSort && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</div>
</Relative>
{expanded && !isDragging && (
<>
{node.children.map((childNode, index) => (
@ -285,17 +286,13 @@ function DocumentLink(
);
}
const Draggable = styled("div")`
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
const Relative = styled.div`
position: relative;
`;
const Disclosure = styled(CollapsedIcon)`
transition: transform 100ms ease, fill 50ms !important;
position: absolute;
left: -24px;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
const Draggable = styled.div`
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
`;
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));

View File

@ -27,7 +27,7 @@ const Cursor = styled("div")`
width: 100%;
height: 14px;
${(props) => (props.from === "collections" ? "top: 15px;" : "bottom: -7px;")}
${(props) => (props.from === "collections" ? "top: 25px;" : "bottom: -7px;")}
background: transparent;
::after {

View File

@ -5,10 +5,11 @@ 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 16px;
margin: 4px 12px;
`;
export default Header;

View File

@ -31,6 +31,7 @@ type Props = {|
activeClassName?: String,
activeStyle?: Object,
className?: string,
scrollIntoViewIfNeeded?: boolean,
exact?: boolean,
isActive?: any,
location?: Location,
@ -52,6 +53,7 @@ const NavLink = ({
location: locationProp,
strict,
style: styleProp,
scrollIntoViewIfNeeded,
to,
...rest
}: Props) => {
@ -83,13 +85,13 @@ const NavLink = ({
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
React.useEffect(() => {
if (isActive && linkRef.current) {
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "instant",
});
}
}, [linkRef, isActive]);
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
const props = {
"aria-current": (isActive && ariaCurrent) || null,

View File

@ -15,6 +15,7 @@ function PlaceholderCollections() {
const Wrapper = styled.div`
margin: 4px 16px;
margin-left: 40px;
width: 75%;
`;

View File

@ -5,7 +5,7 @@ import Flex from "components/Flex";
const Section = styled(Flex)`
position: relative;
flex-direction: column;
margin: 0 8px 20px;
margin: 0 8px 12px;
min-width: ${(props) => props.theme.sidebarMinWidth}px;
flex-shrink: 0;

View File

@ -28,6 +28,7 @@ type Props = {
theme: Theme,
exact?: boolean,
depth?: number,
scrollIntoViewIfNeeded?: boolean,
};
function SidebarLink(
@ -49,12 +50,13 @@ function SidebarLink(
history,
match,
className,
scrollIntoViewIfNeeded,
}: Props,
ref
) {
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
paddingLeft: `${(depth || 0) * 16 + 12}px`,
};
}, [depth]);
@ -73,6 +75,7 @@ function SidebarLink(
<>
<Link
$isActiveDrop={isActiveDrop}
scrollIntoViewIfNeeded={scrollIntoViewIfNeeded}
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
@ -131,6 +134,7 @@ const Link = styled(NavLink)`
padding: 6px 16px;
border-radius: 4px;
transition: background 50ms, color 50ms;
user-select: none;
background: ${(props) =>
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
color: ${(props) =>
@ -156,15 +160,13 @@ const Link = styled(NavLink)`
`}
@media (hover: hover) {
&:hover + ${Actions},
&:active + ${Actions} {
&:hover + ${Actions}, &:active + ${Actions} {
display: inline-flex;
svg {
opacity: 0.75;
}
}
}
&:hover {
color: ${(props) =>

View File

@ -0,0 +1,171 @@
// @flow
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "components/Flex";
import PlaceholderCollections from "./PlaceholderCollections";
import Section from "./Section";
import SidebarLink from "./SidebarLink";
import StarredLink from "./StarredLink";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
const STARRED_PAGINATION_LIMIT = 10;
const STARRED = "STARRED";
function Starred() {
const [isFetching, setIsFetching] = React.useState(false);
const [fetchError, setFetchError] = React.useState();
const [expanded, setExpanded] = React.useState(true);
const [show, setShow] = React.useState("Nothing");
const [offset, setOffset] = React.useState(0);
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
const { showToast } = useToasts();
const { documents } = useStores();
const { t } = useTranslation();
const { fetchStarred, starred } = documents;
const fetchResults = React.useCallback(async () => {
try {
setIsFetching(true);
await fetchStarred({
limit: STARRED_PAGINATION_LIMIT,
offset,
});
} catch (error) {
showToast(t("Starred documents could not be loaded"), {
type: "error",
});
setFetchError(error);
} finally {
setIsFetching(false);
}
}, [fetchStarred, offset, showToast, t]);
useEffect(() => {
let stateInLocal;
try {
stateInLocal = localStorage.getItem(STARRED);
} catch (_) {
// no-op Safari private mode
}
if (!stateInLocal) {
localStorage.setItem(STARRED, expanded ? "true" : "false");
} else {
setExpanded(stateInLocal === "true");
}
}, [expanded]);
useEffect(() => {
setOffset(starred.length);
if (starred.length <= STARRED_PAGINATION_LIMIT) {
setShow("Nothing");
} else if (starred.length >= upperBound) {
setShow("More");
} else if (starred.length < upperBound) {
setShow("Less");
}
}, [starred, upperBound]);
useEffect(() => {
if (offset === 0) {
fetchResults();
}
}, [fetchResults, offset]);
const handleShowMore = React.useCallback(
async (ev) => {
setUpperBound(
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
);
await fetchResults();
},
[fetchResults]
);
const handleShowLess = React.useCallback((ev) => {
setUpperBound(STARRED_PAGINATION_LIMIT);
setShow("More");
}, []);
const handleExpandClick = React.useCallback(
(ev) => {
ev.preventDefault();
ev.stopPropagation();
try {
localStorage.setItem(STARRED, !expanded ? "true" : "false");
} catch (_) {
// no-op Safari private mode
}
setExpanded((prev) => !prev);
},
[expanded]
);
const content = starred.slice(0, upperBound).map((document, index) => {
return (
<StarredLink
key={document.id}
documentId={document.id}
collectionId={document.collectionId}
to={document.url}
title={document.title}
url={document.url}
depth={2}
/>
);
});
if (!starred.length) {
return null;
}
return (
<Section>
<Flex column>
<SidebarLink
onClick={handleExpandClick}
label={t("Starred")}
icon={<Disclosure expanded={expanded} color="currentColor" />}
/>
{expanded && (
<>
{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) && (
<Flex column>
<PlaceholderCollections />
</Flex>
)}
</>
)}
</Flex>
</Section>
);
}
const Disclosure = styled(CollapsedIcon)`
transition: transform 100ms ease, fill 50ms !important;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default observer(Starred);

View File

@ -0,0 +1,102 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { useEffect, useState } from "react";
import styled from "styled-components";
import Fade from "components/Fade";
import useStores from "../../../hooks/useStores";
import Disclosure from "./Disclosure";
import SidebarLink from "./SidebarLink";
import useBoolean from "hooks/useBoolean";
import DocumentMenu from "menus/DocumentMenu";
type Props = {|
depth: number,
title: string,
to: string,
documentId: string,
collectionId: string,
|};
function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
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 childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];
const hasChildDocuments = childDocuments.length > 0;
useEffect(() => {
async function load() {
if (!document) {
await documents.fetch(documentId);
}
}
load();
}, [collection, collectionId, collections, document, documentId, documents]);
const handleDisclosureClick = React.useCallback((ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((prevExpanded) => !prevExpanded);
}, []);
return (
<>
<Relative>
<SidebarLink
depth={depth}
to={to}
label={
<>
{hasChildDocuments && (
<Disclosure
expanded={expanded}
onClick={handleDisclosureClick}
/>
)}
{title}
</>
}
exact={false}
showActions={menuOpen}
menu={
document ? (
<Fade>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Fade>
) : undefined
}
/>
</Relative>
{expanded &&
childDocuments.map((childDocument) => (
<ObserveredStarredLink
key={childDocument.id}
depth={depth + 1}
title={childDocument.title}
to={childDocument.url}
documentId={childDocument.id}
collectionId={collectionId}
/>
))}
</>
);
}
const Relative = styled.div`
position: relative;
`;
const ObserveredStarredLink = observer(StarredLink);
export default ObserveredStarredLink;

View File

@ -8,7 +8,6 @@ import Drafts from "scenes/Drafts";
import Error404 from "scenes/Error404";
import Home from "scenes/Home";
import Search from "scenes/Search";
import Starred from "scenes/Starred";
import Templates from "scenes/Templates";
import Trash from "scenes/Trash";
@ -51,13 +50,12 @@ export default function AuthenticatedRoutes() {
<Redirect from="/dashboard" to="/home" />
<Route path="/home/:tab" component={Home} />
<Route path="/home" component={Home} />
<Route exact path="/starred" component={Starred} />
<Route exact path="/starred/:sort" component={Starred} />
<Route exact path="/templates" component={Templates} />
<Route exact path="/templates/:sort" component={Templates} />
<Route exact path="/drafts" component={Drafts} />
<Route exact path="/archive" component={Archive} />
<Route exact path="/trash" component={Trash} />
<Redirect exact from="/starred" to="/home" />
<Redirect exact from="/collections/*" to="/collection/*" />
<Route exact path="/collection/:id/new" component={DocumentNew} />
<Route exact path="/collection/:id/:tab" component={Collection} />

View File

@ -1,64 +0,0 @@
// @flow
import { observer } from "mobx-react";
import { StarredIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { type Match } from "react-router-dom";
import { Action } from "components/Actions";
import Empty from "components/Empty";
import Heading from "components/Heading";
import InputSearchPage from "components/InputSearchPage";
import PaginatedDocumentList from "components/PaginatedDocumentList";
import Scene from "components/Scene";
import Tab from "components/Tab";
import Tabs from "components/Tabs";
import useStores from "hooks/useStores";
import NewDocumentMenu from "menus/NewDocumentMenu";
type Props = {
match: Match,
};
function Starred(props: Props) {
const { documents } = useStores();
const { t } = useTranslation();
const { fetchStarred, starred, starredAlphabetical } = documents;
const { sort } = props.match.params;
return (
<Scene
icon={<StarredIcon color="currentColor" />}
title={t("Starred")}
actions={
<>
<Action>
<InputSearchPage source="starred" label={t("Search documents")} />
</Action>
<Action>
<NewDocumentMenu />
</Action>
</>
}
>
<Heading>{t("Starred")}</Heading>
<PaginatedDocumentList
heading={
<Tabs>
<Tab to="/starred" exact>
{t("Recently updated")}
</Tab>
<Tab to="/starred/alphabetical" exact>
{t("Alphabetical")}
</Tab>
</Tabs>
}
empty={<Empty>{t("Youve not starred any documents yet.")}</Empty>}
fetch={fetchStarred}
documents={sort === "alphabetical" ? starredAlphabetical : starred}
showCollection
/>
</Scene>
);
}
export default observer(Starred);

View File

@ -7,10 +7,6 @@ export function homeUrl(): string {
return "/home";
}
export function starredUrl(): string {
return "/starred";
}
export function newCollectionUrl(): string {
return "/collection/new";
}

View File

@ -141,9 +141,12 @@
"Collections": "Collections",
"Untitled": "Untitled",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
"Starred documents could not be loaded": "Starred documents could not be loaded",
"Starred": "Starred",
"Show more": "Show more",
"Show less": "Show less",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Home": "Home",
"Starred": "Starred",
"Settings": "Settings",
"Invite people": "Invite people",
"Create a collection": "Create a collection",
@ -175,6 +178,7 @@
"System": "System",
"Light": "Light",
"Dark": "Dark",
"Switch team": "Switch team",
"Log out": "Log out",
"Show path to document": "Show path to document",
"Path to document": "Path to document",
@ -551,7 +555,6 @@
"Zapier": "Zapier",
"Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'": "Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'",
"Open Zapier": "Open Zapier",
"Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
"Trash is empty at the moment.": "Trash is empty at the moment.",

View File

@ -198,7 +198,7 @@ export const dark = {
placeholder: colors.slateDark,
sidebarBackground: colors.veryDarkBlue,
sidebarItemBackground: colors.transparent,
sidebarItemBackground: lighten(0.015, colors.almostBlack),
sidebarText: colors.slate,
backdrop: "rgba(255, 255, 255, 0.3)",
shadow: "rgba(0, 0, 0, 0.6)",