fix: Embed disabled state should persist (#3407)
* Normalize code around localStorage Persist disabled embed state * fix: Cannot view more than 10 starred items on load * More tidying of sidebar state
This commit is contained in:
@@ -22,7 +22,6 @@ function Collections() {
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const { documents, collections } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const isPreloaded = !!collections.orderedData.length;
|
||||
const { t } = useTranslation();
|
||||
const orderedCollections = collections.orderedData;
|
||||
@@ -97,20 +96,18 @@ function Collections() {
|
||||
if (!collections.isLoaded || fetchError) {
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{t("Collections")}</Header>
|
||||
<PlaceholderCollections />
|
||||
<Header id="collections" title={t("Collections")}>
|
||||
<PlaceholderCollections />
|
||||
</Header>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Header onClick={() => setExpanded((prev) => !prev)} expanded={expanded}>
|
||||
{t("Collections")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Header id="collections" title={t("Collections")}>
|
||||
<Relative>{isPreloaded ? content : <Fade>{content}</Fade>}</Relative>
|
||||
)}
|
||||
</Header>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,65 @@
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import usePersistedState from "~/hooks/usePersistedState";
|
||||
|
||||
type Props = {
|
||||
onClick?: React.MouseEventHandler;
|
||||
expanded?: boolean;
|
||||
/** Unique header id – if passed the header will become toggleable */
|
||||
id?: string;
|
||||
title: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Header: React.FC<Props> = ({ onClick, expanded, children }) => {
|
||||
/**
|
||||
* Toggleable sidebar header
|
||||
*/
|
||||
export const Header: React.FC<Props> = ({ id, title, children }) => {
|
||||
const [firstRender, setFirstRender] = React.useState(true);
|
||||
const [expanded, setExpanded] = usePersistedState(
|
||||
`sidebar-header-${id}`,
|
||||
true
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!expanded) {
|
||||
setFirstRender(false);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
setExpanded(!expanded);
|
||||
}, [expanded, setExpanded]);
|
||||
|
||||
return (
|
||||
<H3>
|
||||
<Button onClick={onClick} disabled={!onClick}>
|
||||
{children}
|
||||
{onClick && (
|
||||
<Disclosure expanded={expanded} color="currentColor" size={20} />
|
||||
)}
|
||||
</Button>
|
||||
</H3>
|
||||
<>
|
||||
<H3>
|
||||
<Button onClick={handleClick} disabled={!id}>
|
||||
{title}
|
||||
{id && (
|
||||
<Disclosure expanded={expanded} color="currentColor" size={20} />
|
||||
)}
|
||||
</Button>
|
||||
</H3>
|
||||
{expanded && (firstRender ? children : <Fade>{children}</Fade>)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const fadeAndSlideDown = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
`;
|
||||
|
||||
const Fade = styled.span`
|
||||
animation: ${fadeAndSlideDown} 100ms ease-in-out;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -24,6 +24,7 @@ type Props = Omit<NavLinkProps, "to"> & {
|
||||
label?: React.ReactNode;
|
||||
menu?: React.ReactNode;
|
||||
showActions?: boolean;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
/* If set, a disclosure will be rendered to the left of any icon */
|
||||
expanded?: boolean;
|
||||
@@ -55,6 +56,7 @@ function SidebarLink(
|
||||
className,
|
||||
expanded,
|
||||
onDisclosureClick,
|
||||
disabled,
|
||||
...rest
|
||||
}: Props,
|
||||
ref: React.RefObject<HTMLAnchorElement>
|
||||
@@ -82,6 +84,7 @@ function SidebarLink(
|
||||
<Link
|
||||
$isActiveDrop={isActiveDrop}
|
||||
$isDraft={isDraft}
|
||||
$disabled={disabled}
|
||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
@@ -158,7 +161,11 @@ const Actions = styled(EventBoundary)<{ showActions?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
||||
const Link = styled(NavLink)<{
|
||||
$isActiveDrop?: boolean;
|
||||
$isDraft?: boolean;
|
||||
$disabled?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
@@ -174,6 +181,13 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
${(props) =>
|
||||
props.$disabled &&
|
||||
css`
|
||||
pointer-events: none;
|
||||
opacity: 0.75;
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.$isDraft &&
|
||||
css`
|
||||
|
||||
@@ -16,106 +16,51 @@ import StarredContext from "./StarredContext";
|
||||
import StarredLink from "./StarredLink";
|
||||
|
||||
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 [displayedStarsCount, setDisplayedStarsCount] = React.useState(
|
||||
STARRED_PAGINATION_LIMIT
|
||||
);
|
||||
const { showToast } = useToasts();
|
||||
const { stars } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
await stars.fetchPage({
|
||||
limit: STARRED_PAGINATION_LIMIT,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(t("Starred documents could not be loaded"), {
|
||||
type: "error",
|
||||
});
|
||||
setFetchError(error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}, [stars, offset, showToast, t]);
|
||||
|
||||
React.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]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOffset(stars.orderedData.length);
|
||||
|
||||
if (stars.orderedData.length <= STARRED_PAGINATION_LIMIT) {
|
||||
setShow("Nothing");
|
||||
} else if (stars.orderedData.length >= upperBound) {
|
||||
setShow("More");
|
||||
} else if (stars.orderedData.length < upperBound) {
|
||||
setShow("Less");
|
||||
}
|
||||
}, [stars.orderedData, upperBound]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (offset === 0) {
|
||||
fetchResults();
|
||||
}
|
||||
}, [fetchResults, offset]);
|
||||
|
||||
const handleShowMore = React.useCallback(async () => {
|
||||
setUpperBound(
|
||||
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
|
||||
);
|
||||
await fetchResults();
|
||||
}, [fetchResults]);
|
||||
|
||||
const handleShowLess = React.useCallback(() => {
|
||||
setUpperBound(STARRED_PAGINATION_LIMIT);
|
||||
setShow("More");
|
||||
}, []);
|
||||
|
||||
const handleExpandClick = React.useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const fetchResults = React.useCallback(
|
||||
async (offset = 0) => {
|
||||
try {
|
||||
localStorage.setItem(STARRED, !expanded ? "true" : "false");
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
await stars.fetchPage({
|
||||
limit: STARRED_PAGINATION_LIMIT + 1,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(t("Starred documents could not be loaded"), {
|
||||
type: "error",
|
||||
});
|
||||
setFetchError(error);
|
||||
}
|
||||
|
||||
setExpanded((prev) => !prev);
|
||||
},
|
||||
[expanded]
|
||||
[stars, showToast, t]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchResults();
|
||||
}, [fetchResults]);
|
||||
|
||||
const handleShowMore = async () => {
|
||||
await fetchResults(displayedStarsCount);
|
||||
setDisplayedStarsCount((prev) => prev + STARRED_PAGINATION_LIMIT);
|
||||
};
|
||||
|
||||
// Drop to reorder document
|
||||
const [{ isOverReorder }, dropToReorder] = useDrop({
|
||||
const [{ isOverReorder, isDraggingAnyStar }, dropToReorder] = useDrop({
|
||||
accept: "star",
|
||||
drop: async (item: Star) => {
|
||||
item?.save({ index: fractionalIndex(null, stars.orderedData[0].index) });
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOverReorder: !!monitor.isOver(),
|
||||
isDraggingAnyStar: monitor.getItemType() === "star",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -126,40 +71,33 @@ function Starred() {
|
||||
return (
|
||||
<StarredContext.Provider value={true}>
|
||||
<Flex column>
|
||||
<Header onClick={handleExpandClick} expanded={expanded}>
|
||||
{t("Starred")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Header id="starred" title={t("Starred")}>
|
||||
<Relative>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{stars.orderedData.slice(0, upperBound).map((star) => (
|
||||
{isDraggingAnyStar && (
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{stars.orderedData.slice(0, displayedStarsCount).map((star) => (
|
||||
<StarredLink key={star.id} star={star} />
|
||||
))}
|
||||
{show === "More" && !isFetching && (
|
||||
{stars.orderedData.length > displayedStarsCount && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
disabled={stars.isFetching}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
||||
{(stars.isFetching || fetchError) && !stars.orderedData.length && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</Relative>
|
||||
)}
|
||||
</Header>
|
||||
</Flex>
|
||||
</StarredContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user