diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js
index ae816f0d..8c35d6dd 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -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}
/>
- }
- exact={false}
- label={t("Starred")}
- />
{can.createDocument && (
)}
+
}
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}
/>
))}
diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js
index 3e877796..36f92588 100644
--- a/app/components/Sidebar/components/Collections.js
+++ b/app/components/Sidebar/components/Collections.js
@@ -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={}
label={`${t("New collection")}…`}
exact
+ depth={0.5}
/>
)}
>
@@ -107,7 +109,11 @@ function Collections({ onCreateCollection }: Props) {
if (!collections.isLoaded || fetchError) {
return (
-
+ }
+ disabled
+ />
);
@@ -115,10 +121,19 @@ function Collections({ onCreateCollection }: Props) {
return (
-
- {isPreloaded ? content : {content}}
+ setExpanded((prev) => !prev)}
+ label={t("Collections")}
+ icon={}
+ />
+ {expanded && (isPreloaded ? content : {content})}
);
}
+const Disclosure = styled(CollapsedIcon)`
+ transition: transform 100ms ease, fill 50ms !important;
+ ${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
+`;
+
export default observer(Collections);
diff --git a/app/components/Sidebar/components/Disclosure.js b/app/components/Sidebar/components/Disclosure.js
new file mode 100644
index 00000000..cedba928
--- /dev/null
+++ b/app/components/Sidebar/components/Disclosure.js
@@ -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;
diff --git a/app/components/Sidebar/components/DocumentLink.js b/app/components/Sidebar/components/DocumentLink.js
index 1c6d6798..cd567fbe 100644
--- a/app/components/Sidebar/components/DocumentLink.js
+++ b/app/components/Sidebar/components/DocumentLink.js
@@ -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 (
<>
-
+
)}
-
+
{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));
diff --git a/app/components/Sidebar/components/DropCursor.js b/app/components/Sidebar/components/DropCursor.js
index d25ac49d..4e280d5d 100644
--- a/app/components/Sidebar/components/DropCursor.js
+++ b/app/components/Sidebar/components/DropCursor.js
@@ -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 {
diff --git a/app/components/Sidebar/components/Header.js b/app/components/Sidebar/components/Header.js
index dbc9ccd5..e7f72744 100644
--- a/app/components/Sidebar/components/Header.js
+++ b/app/components/Sidebar/components/Header.js
@@ -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;
diff --git a/app/components/Sidebar/components/NavLink.js b/app/components/Sidebar/components/NavLink.js
index c0b3003a..0c2967e0 100644
--- a/app/components/Sidebar/components/NavLink.js
+++ b/app/components/Sidebar/components/NavLink.js
@@ -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,
diff --git a/app/components/Sidebar/components/PlaceholderCollections.js b/app/components/Sidebar/components/PlaceholderCollections.js
index 8b6375a2..3efadb1f 100644
--- a/app/components/Sidebar/components/PlaceholderCollections.js
+++ b/app/components/Sidebar/components/PlaceholderCollections.js
@@ -15,6 +15,7 @@ function PlaceholderCollections() {
const Wrapper = styled.div`
margin: 4px 16px;
+ margin-left: 40px;
width: 75%;
`;
diff --git a/app/components/Sidebar/components/Section.js b/app/components/Sidebar/components/Section.js
index 2469fa9d..7b1877d9 100644
--- a/app/components/Sidebar/components/Section.js
+++ b/app/components/Sidebar/components/Section.js
@@ -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;
diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js
index 49be59f9..8ebe4082 100644
--- a/app/components/Sidebar/components/SidebarLink.js
+++ b/app/components/Sidebar/components/SidebarLink.js
@@ -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(
<>
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
color: ${(props) =>
@@ -156,13 +160,11 @@ const Link = styled(NavLink)`
`}
@media (hover: hover) {
- &:hover + ${Actions},
- &:active + ${Actions} {
- display: inline-flex;
+ &:hover + ${Actions}, &:active + ${Actions} {
+ display: inline-flex;
- svg {
- opacity: 0.75;
- }
+ svg {
+ opacity: 0.75;
}
}
diff --git a/app/components/Sidebar/components/Starred.js b/app/components/Sidebar/components/Starred.js
new file mode 100644
index 00000000..778f81c9
--- /dev/null
+++ b/app/components/Sidebar/components/Starred.js
@@ -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 (
+
+ );
+ });
+
+ if (!starred.length) {
+ return null;
+ }
+
+ return (
+
+
+ }
+ />
+ {expanded && (
+ <>
+ {content}
+ {show === "More" && !isFetching && (
+
+ )}
+ {show === "Less" && !isFetching && (
+
+ )}
+ {(isFetching || fetchError) && (
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+const Disclosure = styled(CollapsedIcon)`
+ transition: transform 100ms ease, fill 50ms !important;
+ ${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
+`;
+
+export default observer(Starred);
diff --git a/app/components/Sidebar/components/StarredLink.js b/app/components/Sidebar/components/StarredLink.js
new file mode 100644
index 00000000..55d53dca
--- /dev/null
+++ b/app/components/Sidebar/components/StarredLink.js
@@ -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 (
+ <>
+
+
+ {hasChildDocuments && (
+
+ )}
+ {title}
+ >
+ }
+ exact={false}
+ showActions={menuOpen}
+ menu={
+ document ? (
+
+
+
+ ) : undefined
+ }
+ />
+
+ {expanded &&
+ childDocuments.map((childDocument) => (
+
+ ))}
+ >
+ );
+}
+
+const Relative = styled.div`
+ position: relative;
+`;
+
+const ObserveredStarredLink = observer(StarredLink);
+
+export default ObserveredStarredLink;
diff --git a/app/routes/authenticated.js b/app/routes/authenticated.js
index 8e5201cd..7b9dd1bb 100644
--- a/app/routes/authenticated.js
+++ b/app/routes/authenticated.js
@@ -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() {
-
-
+
diff --git a/app/scenes/Starred.js b/app/scenes/Starred.js
deleted file mode 100644
index c875340a..00000000
--- a/app/scenes/Starred.js
+++ /dev/null
@@ -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 (
- }
- title={t("Starred")}
- actions={
- <>
-
-
-
-
-
-
- >
- }
- >
- {t("Starred")}
-
-
- {t("Recently updated")}
-
-
- {t("Alphabetical")}
-
-
- }
- empty={{t("You’ve not starred any documents yet.")}}
- fetch={fetchStarred}
- documents={sort === "alphabetical" ? starredAlphabetical : starred}
- showCollection
- />
-
- );
-}
-
-export default observer(Starred);
diff --git a/app/utils/routeHelpers.js b/app/utils/routeHelpers.js
index f31557d3..d7566f0f 100644
--- a/app/utils/routeHelpers.js
+++ b/app/utils/routeHelpers.js
@@ -7,10 +7,6 @@ export function homeUrl(): string {
return "/home";
}
-export function starredUrl(): string {
- return "/starred";
-}
-
export function newCollectionUrl(): string {
return "/collection/new";
}
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 33e6572a..a0bef38f 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -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",
- "You’ve not starred any documents yet.": "You’ve 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.",
diff --git a/shared/theme.js b/shared/theme.js
index 56f51e87..0e355085 100644
--- a/shared/theme.js
+++ b/shared/theme.js
@@ -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)",