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:
@@ -39,20 +39,21 @@ function SettingsSidebar() {
|
|||||||
<Scrollable shadow>
|
<Scrollable shadow>
|
||||||
{Object.keys(groupedConfig).map((header) => (
|
{Object.keys(groupedConfig).map((header) => (
|
||||||
<Section key={header}>
|
<Section key={header}>
|
||||||
<Header>{header}</Header>
|
<Header title={header}>
|
||||||
{groupedConfig[header].map((item) => (
|
{groupedConfig[header].map((item) => (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
icon={<item.icon color="currentColor" />}
|
icon={<item.icon color="currentColor" />}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</Header>
|
||||||
</Section>
|
</Section>
|
||||||
))}
|
))}
|
||||||
{!isHosted && (
|
{!isHosted && (
|
||||||
<Section>
|
<Section>
|
||||||
<Header>{t("Installation")}</Header>
|
<Header title={t("Installation")} />
|
||||||
<Version />
|
<Version />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ function Collections() {
|
|||||||
const [fetchError, setFetchError] = React.useState();
|
const [fetchError, setFetchError] = React.useState();
|
||||||
const { documents, collections } = useStores();
|
const { documents, collections } = useStores();
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
const [expanded, setExpanded] = React.useState(true);
|
|
||||||
const isPreloaded = !!collections.orderedData.length;
|
const isPreloaded = !!collections.orderedData.length;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const orderedCollections = collections.orderedData;
|
const orderedCollections = collections.orderedData;
|
||||||
@@ -97,20 +96,18 @@ function Collections() {
|
|||||||
if (!collections.isLoaded || fetchError) {
|
if (!collections.isLoaded || fetchError) {
|
||||||
return (
|
return (
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header>{t("Collections")}</Header>
|
<Header id="collections" title={t("Collections")}>
|
||||||
<PlaceholderCollections />
|
<PlaceholderCollections />
|
||||||
|
</Header>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header onClick={() => setExpanded((prev) => !prev)} expanded={expanded}>
|
<Header id="collections" title={t("Collections")}>
|
||||||
{t("Collections")}
|
|
||||||
</Header>
|
|
||||||
{expanded && (
|
|
||||||
<Relative>{isPreloaded ? content : <Fade>{content}</Fade>}</Relative>
|
<Relative>{isPreloaded ? content : <Fade>{content}</Fade>}</Relative>
|
||||||
)}
|
</Header>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,65 @@
|
|||||||
import { CollapsedIcon } from "outline-icons";
|
import { CollapsedIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled, { keyframes } from "styled-components";
|
||||||
|
import usePersistedState from "~/hooks/usePersistedState";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick?: React.MouseEventHandler;
|
/** Unique header id – if passed the header will become toggleable */
|
||||||
expanded?: boolean;
|
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 (
|
return (
|
||||||
<H3>
|
<>
|
||||||
<Button onClick={onClick} disabled={!onClick}>
|
<H3>
|
||||||
{children}
|
<Button onClick={handleClick} disabled={!id}>
|
||||||
{onClick && (
|
{title}
|
||||||
<Disclosure expanded={expanded} color="currentColor" size={20} />
|
{id && (
|
||||||
)}
|
<Disclosure expanded={expanded} color="currentColor" size={20} />
|
||||||
</Button>
|
)}
|
||||||
</H3>
|
</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`
|
const Button = styled.button`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Props = Omit<NavLinkProps, "to"> & {
|
|||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
menu?: React.ReactNode;
|
menu?: React.ReactNode;
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
/* If set, a disclosure will be rendered to the left of any icon */
|
/* If set, a disclosure will be rendered to the left of any icon */
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
@@ -55,6 +56,7 @@ function SidebarLink(
|
|||||||
className,
|
className,
|
||||||
expanded,
|
expanded,
|
||||||
onDisclosureClick,
|
onDisclosureClick,
|
||||||
|
disabled,
|
||||||
...rest
|
...rest
|
||||||
}: Props,
|
}: Props,
|
||||||
ref: React.RefObject<HTMLAnchorElement>
|
ref: React.RefObject<HTMLAnchorElement>
|
||||||
@@ -82,6 +84,7 @@ function SidebarLink(
|
|||||||
<Link
|
<Link
|
||||||
$isActiveDrop={isActiveDrop}
|
$isActiveDrop={isActiveDrop}
|
||||||
$isDraft={isDraft}
|
$isDraft={isDraft}
|
||||||
|
$disabled={disabled}
|
||||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||||
style={active ? activeStyle : style}
|
style={active ? activeStyle : style}
|
||||||
onClick={onClick}
|
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;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -174,6 +181,13 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.$disabled &&
|
||||||
|
css`
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.75;
|
||||||
|
`}
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.$isDraft &&
|
props.$isDraft &&
|
||||||
css`
|
css`
|
||||||
|
|||||||
@@ -16,106 +16,51 @@ import StarredContext from "./StarredContext";
|
|||||||
import StarredLink from "./StarredLink";
|
import StarredLink from "./StarredLink";
|
||||||
|
|
||||||
const STARRED_PAGINATION_LIMIT = 10;
|
const STARRED_PAGINATION_LIMIT = 10;
|
||||||
const STARRED = "STARRED";
|
|
||||||
|
|
||||||
function Starred() {
|
function Starred() {
|
||||||
const [isFetching, setIsFetching] = React.useState(false);
|
|
||||||
const [fetchError, setFetchError] = React.useState();
|
const [fetchError, setFetchError] = React.useState();
|
||||||
const [expanded, setExpanded] = React.useState(true);
|
const [displayedStarsCount, setDisplayedStarsCount] = React.useState(
|
||||||
const [show, setShow] = React.useState("Nothing");
|
STARRED_PAGINATION_LIMIT
|
||||||
const [offset, setOffset] = React.useState(0);
|
);
|
||||||
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
|
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
const { stars } = useStores();
|
const { stars } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const fetchResults = React.useCallback(async () => {
|
const fetchResults = React.useCallback(
|
||||||
try {
|
async (offset = 0) => {
|
||||||
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();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STARRED, !expanded ? "true" : "false");
|
await stars.fetchPage({
|
||||||
} catch (_) {
|
limit: STARRED_PAGINATION_LIMIT + 1,
|
||||||
// no-op Safari private mode
|
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
|
// Drop to reorder document
|
||||||
const [{ isOverReorder }, dropToReorder] = useDrop({
|
const [{ isOverReorder, isDraggingAnyStar }, dropToReorder] = useDrop({
|
||||||
accept: "star",
|
accept: "star",
|
||||||
drop: async (item: Star) => {
|
drop: async (item: Star) => {
|
||||||
item?.save({ index: fractionalIndex(null, stars.orderedData[0].index) });
|
item?.save({ index: fractionalIndex(null, stars.orderedData[0].index) });
|
||||||
},
|
},
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
isOverReorder: !!monitor.isOver(),
|
isOverReorder: !!monitor.isOver(),
|
||||||
|
isDraggingAnyStar: monitor.getItemType() === "star",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,40 +71,33 @@ function Starred() {
|
|||||||
return (
|
return (
|
||||||
<StarredContext.Provider value={true}>
|
<StarredContext.Provider value={true}>
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Header onClick={handleExpandClick} expanded={expanded}>
|
<Header id="starred" title={t("Starred")}>
|
||||||
{t("Starred")}
|
|
||||||
</Header>
|
|
||||||
{expanded && (
|
|
||||||
<Relative>
|
<Relative>
|
||||||
<DropCursor
|
{isDraggingAnyStar && (
|
||||||
isActiveDrop={isOverReorder}
|
<DropCursor
|
||||||
innerRef={dropToReorder}
|
isActiveDrop={isOverReorder}
|
||||||
position="top"
|
innerRef={dropToReorder}
|
||||||
/>
|
position="top"
|
||||||
{stars.orderedData.slice(0, upperBound).map((star) => (
|
/>
|
||||||
|
)}
|
||||||
|
{stars.orderedData.slice(0, displayedStarsCount).map((star) => (
|
||||||
<StarredLink key={star.id} star={star} />
|
<StarredLink key={star.id} star={star} />
|
||||||
))}
|
))}
|
||||||
{show === "More" && !isFetching && (
|
{stars.orderedData.length > displayedStarsCount && (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
onClick={handleShowMore}
|
onClick={handleShowMore}
|
||||||
label={`${t("Show more")}…`}
|
label={`${t("Show more")}…`}
|
||||||
|
disabled={stars.isFetching}
|
||||||
depth={0}
|
depth={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{show === "Less" && !isFetching && (
|
{(stars.isFetching || fetchError) && !stars.orderedData.length && (
|
||||||
<SidebarLink
|
|
||||||
onClick={handleShowLess}
|
|
||||||
label={`${t("Show less")}…`}
|
|
||||||
depth={0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<PlaceholderCollections />
|
<PlaceholderCollections />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Relative>
|
</Relative>
|
||||||
)}
|
</Header>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StarredContext.Provider>
|
</StarredContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
51
app/hooks/usePersistedState.ts
Normal file
51
app/hooks/usePersistedState.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Primitive } from "utility-types";
|
||||||
|
import Storage from "~/utils/Storage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook with the same API as `useState` that persists its value locally and
|
||||||
|
* syncs the value between browser tabs.
|
||||||
|
*
|
||||||
|
* @param key Key to store value under
|
||||||
|
* @param defaultValue An optional default value if no key exists
|
||||||
|
* @returns Tuple of the current value and a function to update it
|
||||||
|
*/
|
||||||
|
export default function usePersistedState(
|
||||||
|
key: string,
|
||||||
|
defaultValue: Primitive
|
||||||
|
) {
|
||||||
|
const [storedValue, setStoredValue] = React.useState(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return Storage.get(key) ?? defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setValue = (value: Primitive | ((value: Primitive) => void)) => {
|
||||||
|
try {
|
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore =
|
||||||
|
value instanceof Function ? value(storedValue) : value;
|
||||||
|
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
Storage.set(key, valueToStore);
|
||||||
|
} catch (error) {
|
||||||
|
// A more advanced implementation would handle the error case
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to the key changing in other tabs so we can keep UI in sync
|
||||||
|
React.useEffect(() => {
|
||||||
|
const updateValue = (event: any) => {
|
||||||
|
if (event.key === key && event.newValue) {
|
||||||
|
setStoredValue(JSON.parse(event.newValue));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", updateValue);
|
||||||
|
return () => window.removeEventListener("storage", updateValue);
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { addDays, differenceInDays } from "date-fns";
|
import { addDays, differenceInDays } from "date-fns";
|
||||||
import { floor } from "lodash";
|
import { floor } from "lodash";
|
||||||
import { action, computed, observable } from "mobx";
|
import { action, autorun, computed, observable } from "mobx";
|
||||||
import parseTitle from "@shared/utils/parseTitle";
|
import parseTitle from "@shared/utils/parseTitle";
|
||||||
import unescape from "@shared/utils/unescape";
|
import unescape from "@shared/utils/unescape";
|
||||||
import DocumentsStore from "~/stores/DocumentsStore";
|
import DocumentsStore from "~/stores/DocumentsStore";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import { NavigationNode } from "~/types";
|
import { NavigationNode } from "~/types";
|
||||||
|
import Storage from "~/utils/Storage";
|
||||||
import ParanoidModel from "./ParanoidModel";
|
import ParanoidModel from "./ParanoidModel";
|
||||||
import View from "./View";
|
import View from "./View";
|
||||||
import Field from "./decorators/Field";
|
import Field from "./decorators/Field";
|
||||||
@@ -18,11 +19,28 @@ type SaveOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class Document extends ParanoidModel {
|
export default class Document extends ParanoidModel {
|
||||||
|
constructor(fields: Record<string, any>, store: DocumentsStore) {
|
||||||
|
super(fields, store);
|
||||||
|
|
||||||
|
if (this.isPersistedOnce && this.isFromTemplate) {
|
||||||
|
this.title = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.embedsDisabled = Storage.get(`embedsDisabled-${this.id}`) ?? false;
|
||||||
|
|
||||||
|
autorun(() => {
|
||||||
|
Storage.set(
|
||||||
|
`embedsDisabled-${this.id}`,
|
||||||
|
this.embedsDisabled ? true : undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
isSaving = false;
|
isSaving = false;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
embedsDisabled = false;
|
embedsDisabled: boolean;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
lastViewedAt: string | undefined;
|
lastViewedAt: string | undefined;
|
||||||
@@ -82,14 +100,6 @@ export default class Document extends ParanoidModel {
|
|||||||
|
|
||||||
revision: number;
|
revision: number;
|
||||||
|
|
||||||
constructor(fields: Record<string, any>, store: DocumentsStore) {
|
|
||||||
super(fields, store);
|
|
||||||
|
|
||||||
if (this.isPersistedOnce && this.isFromTemplate) {
|
|
||||||
this.title = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get emoji() {
|
get emoji() {
|
||||||
const { emoji } = parseTitle(this.title);
|
const { emoji } = parseTitle(this.title);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Team from "~/models/Team";
|
|||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
import { client } from "~/utils/ApiClient";
|
import { client } from "~/utils/ApiClient";
|
||||||
|
import Storage from "~/utils/Storage";
|
||||||
import { getCookieDomain } from "~/utils/domains";
|
import { getCookieDomain } from "~/utils/domains";
|
||||||
|
|
||||||
const AUTH_STORE = "AUTH_STORE";
|
const AUTH_STORE = "AUTH_STORE";
|
||||||
@@ -64,23 +65,13 @@ export default class AuthStore {
|
|||||||
constructor(rootStore: RootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
this.rootStore = rootStore;
|
this.rootStore = rootStore;
|
||||||
// attempt to load the previous state of this store from localstorage
|
// attempt to load the previous state of this store from localstorage
|
||||||
let data: PersistedData = {};
|
const data: PersistedData = Storage.get(AUTH_STORE) || {};
|
||||||
|
|
||||||
try {
|
|
||||||
data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}");
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rehydrate(data);
|
this.rehydrate(data);
|
||||||
|
|
||||||
// persists this entire store to localstorage whenever any keys are changed
|
// persists this entire store to localstorage whenever any keys are changed
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
try {
|
Storage.set(AUTH_STORE, this.asJson);
|
||||||
localStorage.setItem(AUTH_STORE, this.asJson);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen to the localstorage value changing in other tabs to react to
|
// listen to the localstorage value changing in other tabs to react to
|
||||||
@@ -135,12 +126,12 @@ export default class AuthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get asJson(): string {
|
get asJson() {
|
||||||
return JSON.stringify({
|
return {
|
||||||
user: this.user,
|
user: this.user,
|
||||||
team: this.team,
|
team: this.team,
|
||||||
policies: this.policies,
|
policies: this.policies,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -248,14 +239,11 @@ export default class AuthStore {
|
|||||||
@action
|
@action
|
||||||
logout = async (savePath = false) => {
|
logout = async (savePath = false) => {
|
||||||
// remove user and team from localStorage
|
// remove user and team from localStorage
|
||||||
localStorage.setItem(
|
Storage.set(AUTH_STORE, {
|
||||||
AUTH_STORE,
|
user: null,
|
||||||
JSON.stringify({
|
team: null,
|
||||||
user: null,
|
policies: [],
|
||||||
team: null,
|
});
|
||||||
policies: [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.token = null;
|
this.token = null;
|
||||||
|
|
||||||
// if this logout was forced from an authenticated route then
|
// if this logout was forced from an authenticated route then
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { action, autorun, computed, observable } from "mobx";
|
|||||||
import { light as defaultTheme } from "@shared/styles/theme";
|
import { light as defaultTheme } from "@shared/styles/theme";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import { ConnectionStatus } from "~/scenes/Document/components/MultiplayerEditor";
|
import { ConnectionStatus } from "~/scenes/Document/components/MultiplayerEditor";
|
||||||
|
import Storage from "~/utils/Storage";
|
||||||
|
|
||||||
const UI_STORE = "UI_STORE";
|
const UI_STORE = "UI_STORE";
|
||||||
|
|
||||||
@@ -67,13 +68,7 @@ class UiStore {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Rehydrate
|
// Rehydrate
|
||||||
let data: Partial<UiStore> = {};
|
const data: Partial<UiStore> = Storage.get(UI_STORE) || {};
|
||||||
|
|
||||||
try {
|
|
||||||
data = JSON.parse(localStorage.getItem(UI_STORE) || "{}");
|
|
||||||
} catch (_) {
|
|
||||||
// no-op Safari private mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// system theme listeners
|
// system theme listeners
|
||||||
if (window.matchMedia) {
|
if (window.matchMedia) {
|
||||||
@@ -100,21 +95,14 @@ class UiStore {
|
|||||||
this.theme = data.theme || Theme.System;
|
this.theme = data.theme || Theme.System;
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
try {
|
Storage.set(UI_STORE, this.asJson);
|
||||||
localStorage.setItem(UI_STORE, this.asJson);
|
|
||||||
} catch (_) {
|
|
||||||
// no-op Safari private mode
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setTheme = (theme: Theme) => {
|
setTheme = (theme: Theme) => {
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
|
Storage.set("theme", this.theme);
|
||||||
if (window.localStorage) {
|
|
||||||
window.localStorage.setItem("theme", this.theme);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -244,14 +232,14 @@ class UiStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get asJson(): string {
|
get asJson() {
|
||||||
return JSON.stringify({
|
return {
|
||||||
tocVisible: this.tocVisible,
|
tocVisible: this.tocVisible,
|
||||||
sidebarCollapsed: this.sidebarCollapsed,
|
sidebarCollapsed: this.sidebarCollapsed,
|
||||||
sidebarWidth: this.sidebarWidth,
|
sidebarWidth: this.sidebarWidth,
|
||||||
languagePromptDismissed: this.languagePromptDismissed,
|
languagePromptDismissed: this.languagePromptDismissed,
|
||||||
theme: this.theme,
|
theme: this.theme,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
app/utils/Storage.ts
Normal file
56
app/utils/Storage.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Storage is a wrapper class for localStorage that allow safe usage when
|
||||||
|
* localStorage is not available.
|
||||||
|
*/
|
||||||
|
export default class Storage {
|
||||||
|
/**
|
||||||
|
* Set a value in localStorage. For efficiency, this method will remove the
|
||||||
|
* value if it is undefined.
|
||||||
|
*
|
||||||
|
* @param key The key to set under.
|
||||||
|
* @param value The value to set
|
||||||
|
*/
|
||||||
|
static set<T>(key: string, value: T) {
|
||||||
|
try {
|
||||||
|
if (value === undefined) {
|
||||||
|
this.remove(key);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// no-op Safari private mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from localStorage.
|
||||||
|
*
|
||||||
|
* @param key The key to get.
|
||||||
|
* @returns The value or undefined if it doesn't exist.
|
||||||
|
*/
|
||||||
|
static get(key: string) {
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// no-op Safari private mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a value from localStorage.
|
||||||
|
*
|
||||||
|
* @param key The key to remove.
|
||||||
|
*/
|
||||||
|
static remove(key: string) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
// no-op Safari private mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,14 +44,6 @@ export default class Embed extends Node {
|
|||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
tag: "a.disabled-embed",
|
|
||||||
getAttrs: (dom: HTMLAnchorElement) => {
|
|
||||||
return {
|
|
||||||
href: dom.getAttribute("href") || "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
toDOM: (node) => [
|
toDOM: (node) => [
|
||||||
"iframe",
|
"iframe",
|
||||||
|
|||||||
Reference in New Issue
Block a user