fix: Show starred docs in sidebar (#2317)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@ -2,12 +2,11 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import {
|
import {
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
SearchIcon,
|
||||||
|
ShapesIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
ShapesIcon,
|
|
||||||
StarredIcon,
|
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
@ -25,6 +24,7 @@ import ArchiveLink from "./components/ArchiveLink";
|
|||||||
import Collections from "./components/Collections";
|
import Collections from "./components/Collections";
|
||||||
import Section from "./components/Section";
|
import Section from "./components/Section";
|
||||||
import SidebarLink from "./components/SidebarLink";
|
import SidebarLink from "./components/SidebarLink";
|
||||||
|
import Starred from "./components/Starred";
|
||||||
import TeamButton from "./components/TeamButton";
|
import TeamButton from "./components/TeamButton";
|
||||||
import TrashLink from "./components/TrashLink";
|
import TrashLink from "./components/TrashLink";
|
||||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
@ -109,12 +109,6 @@ function MainSidebar() {
|
|||||||
label={t("Search")}
|
label={t("Search")}
|
||||||
exact={false}
|
exact={false}
|
||||||
/>
|
/>
|
||||||
<SidebarLink
|
|
||||||
to="/starred"
|
|
||||||
icon={<StarredIcon color="currentColor" />}
|
|
||||||
exact={false}
|
|
||||||
label={t("Starred")}
|
|
||||||
/>
|
|
||||||
{can.createDocument && (
|
{can.createDocument && (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to="/drafts"
|
to="/drafts"
|
||||||
@ -135,6 +129,7 @@ function MainSidebar() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
<Starred />
|
||||||
<Section auto>
|
<Section auto>
|
||||||
<Collections
|
<Collections
|
||||||
onCreateCollection={handleCreateCollectionModalOpen}
|
onCreateCollection={handleCreateCollectionModalOpen}
|
||||||
|
@ -159,6 +159,7 @@ function CollectionLink({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
exact={false}
|
exact={false}
|
||||||
|
depth={0.5}
|
||||||
menu={
|
menu={
|
||||||
<>
|
<>
|
||||||
{can.update && (
|
{can.update && (
|
||||||
@ -198,7 +199,7 @@ function CollectionLink({
|
|||||||
activeDocument={activeDocument}
|
activeDocument={activeDocument}
|
||||||
prefetchDocument={prefetchDocument}
|
prefetchDocument={prefetchDocument}
|
||||||
canUpdate={canUpdate}
|
canUpdate={canUpdate}
|
||||||
depth={1.5}
|
depth={2}
|
||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import fractionalIndex from "fractional-index";
|
import fractionalIndex from "fractional-index";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { PlusIcon } from "outline-icons";
|
import { PlusIcon, CollapsedIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useDrop } from "react-dnd";
|
import { useDrop } from "react-dnd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styled from "styled-components";
|
||||||
import Fade from "components/Fade";
|
import Fade from "components/Fade";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import useStores from "../../../hooks/useStores";
|
import useStores from "../../../hooks/useStores";
|
||||||
import CollectionLink from "./CollectionLink";
|
import CollectionLink from "./CollectionLink";
|
||||||
import DropCursor from "./DropCursor";
|
import DropCursor from "./DropCursor";
|
||||||
import Header from "./Header";
|
|
||||||
import PlaceholderCollections from "./PlaceholderCollections";
|
import PlaceholderCollections from "./PlaceholderCollections";
|
||||||
import SidebarLink from "./SidebarLink";
|
import SidebarLink from "./SidebarLink";
|
||||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
@ -25,6 +25,7 @@ function Collections({ onCreateCollection }: Props) {
|
|||||||
const [fetchError, setFetchError] = React.useState();
|
const [fetchError, setFetchError] = React.useState();
|
||||||
const { ui, policies, documents, collections } = useStores();
|
const { ui, policies, documents, collections } = useStores();
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
|
const [expanded, setExpanded] = React.useState(true);
|
||||||
const isPreloaded: boolean = !!collections.orderedData.length;
|
const isPreloaded: boolean = !!collections.orderedData.length;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
@ -99,6 +100,7 @@ function Collections({ onCreateCollection }: Props) {
|
|||||||
icon={<PlusIcon color="currentColor" />}
|
icon={<PlusIcon color="currentColor" />}
|
||||||
label={`${t("New collection")}…`}
|
label={`${t("New collection")}…`}
|
||||||
exact
|
exact
|
||||||
|
depth={0.5}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -107,7 +109,11 @@ function Collections({ onCreateCollection }: Props) {
|
|||||||
if (!collections.isLoaded || fetchError) {
|
if (!collections.isLoaded || fetchError) {
|
||||||
return (
|
return (
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header>{t("Collections")}</Header>
|
<SidebarLink
|
||||||
|
label={t("Collections")}
|
||||||
|
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
<PlaceholderCollections />
|
<PlaceholderCollections />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@ -115,10 +121,19 @@ function Collections({ onCreateCollection }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header>{t("Collections")}</Header>
|
<SidebarLink
|
||||||
{isPreloaded ? content : <Fade>{content}</Fade>}
|
onClick={() => setExpanded((prev) => !prev)}
|
||||||
|
label={t("Collections")}
|
||||||
|
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||||
|
/>
|
||||||
|
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Disclosure = styled(CollapsedIcon)`
|
||||||
|
transition: transform 100ms ease, fill 50ms !important;
|
||||||
|
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||||
|
`;
|
||||||
|
|
||||||
export default observer(Collections);
|
export default observer(Collections);
|
||||||
|
13
app/components/Sidebar/components/Disclosure.js
Normal file
13
app/components/Sidebar/components/Disclosure.js
Normal 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;
|
@ -1,6 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CollapsedIcon } from "outline-icons";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useDrag, useDrop } from "react-dnd";
|
import { useDrag, useDrop } from "react-dnd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -8,6 +7,7 @@ import styled from "styled-components";
|
|||||||
import Collection from "models/Collection";
|
import Collection from "models/Collection";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import Fade from "components/Fade";
|
import Fade from "components/Fade";
|
||||||
|
import Disclosure from "./Disclosure";
|
||||||
import DropCursor from "./DropCursor";
|
import DropCursor from "./DropCursor";
|
||||||
import DropToImport from "./DropToImport";
|
import DropToImport from "./DropToImport";
|
||||||
import EditableTitle from "./EditableTitle";
|
import EditableTitle from "./EditableTitle";
|
||||||
@ -210,7 +210,7 @@ function DocumentLink(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ position: "relative" }} onDragLeave={resetHoverExpanding}>
|
<Relative onDragLeave={resetHoverExpanding}>
|
||||||
<Draggable
|
<Draggable
|
||||||
key={node.id}
|
key={node.id}
|
||||||
ref={drag}
|
ref={drag}
|
||||||
@ -244,6 +244,7 @@ function DocumentLink(
|
|||||||
depth={depth}
|
depth={depth}
|
||||||
exact={false}
|
exact={false}
|
||||||
showActions={menuOpen}
|
showActions={menuOpen}
|
||||||
|
scrollIntoViewIfNeeded={!document?.isStarred}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
menu={
|
menu={
|
||||||
document && !isMoving ? (
|
document && !isMoving ? (
|
||||||
@ -263,7 +264,7 @@ function DocumentLink(
|
|||||||
{manualSort && (
|
{manualSort && (
|
||||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</Relative>
|
||||||
{expanded && !isDragging && (
|
{expanded && !isDragging && (
|
||||||
<>
|
<>
|
||||||
{node.children.map((childNode, index) => (
|
{node.children.map((childNode, index) => (
|
||||||
@ -285,17 +286,13 @@ function DocumentLink(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Draggable = styled("div")`
|
const Relative = styled.div`
|
||||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
position: relative;
|
||||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Disclosure = styled(CollapsedIcon)`
|
const Draggable = styled.div`
|
||||||
transition: transform 100ms ease, fill 50ms !important;
|
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||||
position: absolute;
|
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||||
left: -24px;
|
|
||||||
|
|
||||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
||||||
|
@ -27,7 +27,7 @@ const Cursor = styled("div")`
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
${(props) => (props.from === "collections" ? "top: 15px;" : "bottom: -7px;")}
|
${(props) => (props.from === "collections" ? "top: 25px;" : "bottom: -7px;")}
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
::after {
|
::after {
|
||||||
|
@ -5,10 +5,11 @@ import Flex from "components/Flex";
|
|||||||
const Header = styled(Flex)`
|
const Header = styled(Flex)`
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: ${(props) => props.theme.sidebarText};
|
color: ${(props) => props.theme.sidebarText};
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
margin: 4px 16px;
|
margin: 4px 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
@ -31,6 +31,7 @@ type Props = {|
|
|||||||
activeClassName?: String,
|
activeClassName?: String,
|
||||||
activeStyle?: Object,
|
activeStyle?: Object,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
scrollIntoViewIfNeeded?: boolean,
|
||||||
exact?: boolean,
|
exact?: boolean,
|
||||||
isActive?: any,
|
isActive?: any,
|
||||||
location?: Location,
|
location?: Location,
|
||||||
@ -52,6 +53,7 @@ const NavLink = ({
|
|||||||
location: locationProp,
|
location: locationProp,
|
||||||
strict,
|
strict,
|
||||||
style: styleProp,
|
style: styleProp,
|
||||||
|
scrollIntoViewIfNeeded,
|
||||||
to,
|
to,
|
||||||
...rest
|
...rest
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@ -83,13 +85,13 @@ const NavLink = ({
|
|||||||
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
|
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isActive && linkRef.current) {
|
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
|
||||||
scrollIntoView(linkRef.current, {
|
scrollIntoView(linkRef.current, {
|
||||||
scrollMode: "if-needed",
|
scrollMode: "if-needed",
|
||||||
behavior: "instant",
|
behavior: "instant",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [linkRef, isActive]);
|
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
"aria-current": (isActive && ariaCurrent) || null,
|
"aria-current": (isActive && ariaCurrent) || null,
|
||||||
|
@ -15,6 +15,7 @@ function PlaceholderCollections() {
|
|||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
margin: 4px 16px;
|
margin: 4px 16px;
|
||||||
|
margin-left: 40px;
|
||||||
width: 75%;
|
width: 75%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import Flex from "components/Flex";
|
|||||||
const Section = styled(Flex)`
|
const Section = styled(Flex)`
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0 8px 20px;
|
margin: 0 8px 12px;
|
||||||
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ type Props = {
|
|||||||
theme: Theme,
|
theme: Theme,
|
||||||
exact?: boolean,
|
exact?: boolean,
|
||||||
depth?: number,
|
depth?: number,
|
||||||
|
scrollIntoViewIfNeeded?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SidebarLink(
|
function SidebarLink(
|
||||||
@ -49,12 +50,13 @@ function SidebarLink(
|
|||||||
history,
|
history,
|
||||||
match,
|
match,
|
||||||
className,
|
className,
|
||||||
|
scrollIntoViewIfNeeded,
|
||||||
}: Props,
|
}: Props,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const style = React.useMemo(() => {
|
const style = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
paddingLeft: `${(depth || 0) * 16 + 16}px`,
|
paddingLeft: `${(depth || 0) * 16 + 12}px`,
|
||||||
};
|
};
|
||||||
}, [depth]);
|
}, [depth]);
|
||||||
|
|
||||||
@ -73,6 +75,7 @@ function SidebarLink(
|
|||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
$isActiveDrop={isActiveDrop}
|
$isActiveDrop={isActiveDrop}
|
||||||
|
scrollIntoViewIfNeeded={scrollIntoViewIfNeeded}
|
||||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||||
style={active ? activeStyle : style}
|
style={active ? activeStyle : style}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@ -131,6 +134,7 @@ const Link = styled(NavLink)`
|
|||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 50ms, color 50ms;
|
transition: background 50ms, color 50ms;
|
||||||
|
user-select: none;
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
|
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
|
||||||
color: ${(props) =>
|
color: ${(props) =>
|
||||||
@ -156,13 +160,11 @@ const Link = styled(NavLink)`
|
|||||||
`}
|
`}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
&:hover + ${Actions},
|
&:hover + ${Actions}, &:active + ${Actions} {
|
||||||
&:active + ${Actions} {
|
display: inline-flex;
|
||||||
display: inline-flex;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
171
app/components/Sidebar/components/Starred.js
Normal file
171
app/components/Sidebar/components/Starred.js
Normal 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);
|
102
app/components/Sidebar/components/StarredLink.js
Normal file
102
app/components/Sidebar/components/StarredLink.js
Normal 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;
|
@ -8,7 +8,6 @@ import Drafts from "scenes/Drafts";
|
|||||||
import Error404 from "scenes/Error404";
|
import Error404 from "scenes/Error404";
|
||||||
import Home from "scenes/Home";
|
import Home from "scenes/Home";
|
||||||
import Search from "scenes/Search";
|
import Search from "scenes/Search";
|
||||||
import Starred from "scenes/Starred";
|
|
||||||
import Templates from "scenes/Templates";
|
import Templates from "scenes/Templates";
|
||||||
import Trash from "scenes/Trash";
|
import Trash from "scenes/Trash";
|
||||||
|
|
||||||
@ -51,13 +50,12 @@ export default function AuthenticatedRoutes() {
|
|||||||
<Redirect from="/dashboard" to="/home" />
|
<Redirect from="/dashboard" to="/home" />
|
||||||
<Route path="/home/:tab" component={Home} />
|
<Route path="/home/:tab" component={Home} />
|
||||||
<Route path="/home" 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" component={Templates} />
|
||||||
<Route exact path="/templates/:sort" component={Templates} />
|
<Route exact path="/templates/:sort" component={Templates} />
|
||||||
<Route exact path="/drafts" component={Drafts} />
|
<Route exact path="/drafts" component={Drafts} />
|
||||||
<Route exact path="/archive" component={Archive} />
|
<Route exact path="/archive" component={Archive} />
|
||||||
<Route exact path="/trash" component={Trash} />
|
<Route exact path="/trash" component={Trash} />
|
||||||
|
<Redirect exact from="/starred" to="/home" />
|
||||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||||
<Route exact path="/collection/:id/:tab" component={Collection} />
|
<Route exact path="/collection/:id/:tab" component={Collection} />
|
||||||
|
@ -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("You’ve not starred any documents yet.")}</Empty>}
|
|
||||||
fetch={fetchStarred}
|
|
||||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
|
||||||
showCollection
|
|
||||||
/>
|
|
||||||
</Scene>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default observer(Starred);
|
|
@ -7,10 +7,6 @@ export function homeUrl(): string {
|
|||||||
return "/home";
|
return "/home";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function starredUrl(): string {
|
|
||||||
return "/starred";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newCollectionUrl(): string {
|
export function newCollectionUrl(): string {
|
||||||
return "/collection/new";
|
return "/collection/new";
|
||||||
}
|
}
|
||||||
|
@ -141,9 +141,12 @@
|
|||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"Untitled": "Untitled",
|
"Untitled": "Untitled",
|
||||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
"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 }}",
|
"Delete {{ documentName }}": "Delete {{ documentName }}",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Starred": "Starred",
|
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Invite people": "Invite people",
|
"Invite people": "Invite people",
|
||||||
"Create a collection": "Create a collection",
|
"Create a collection": "Create a collection",
|
||||||
@ -175,6 +178,7 @@
|
|||||||
"System": "System",
|
"System": "System",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Switch team": "Switch team",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
"Show path to document": "Show path to document",
|
"Show path to document": "Show path to document",
|
||||||
"Path to document": "Path to document",
|
"Path to document": "Path to document",
|
||||||
@ -551,7 +555,6 @@
|
|||||||
"Zapier": "Zapier",
|
"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.'",
|
"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",
|
"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.",
|
"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.",
|
"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.",
|
"Trash is empty at the moment.": "Trash is empty at the moment.",
|
||||||
|
@ -198,7 +198,7 @@ export const dark = {
|
|||||||
placeholder: colors.slateDark,
|
placeholder: colors.slateDark,
|
||||||
|
|
||||||
sidebarBackground: colors.veryDarkBlue,
|
sidebarBackground: colors.veryDarkBlue,
|
||||||
sidebarItemBackground: colors.transparent,
|
sidebarItemBackground: lighten(0.015, colors.almostBlack),
|
||||||
sidebarText: colors.slate,
|
sidebarText: colors.slate,
|
||||||
backdrop: "rgba(255, 255, 255, 0.3)",
|
backdrop: "rgba(255, 255, 255, 0.3)",
|
||||||
shadow: "rgba(0, 0, 0, 0.6)",
|
shadow: "rgba(0, 0, 0, 0.6)",
|
||||||
|
Reference in New Issue
Block a user