Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist link display settings to workspace #1932

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/lib/swr/use-workspace-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useWorkspace from "./use-workspace";

export const STORE_KEYS = {
conversionsOnboarding: "conversionsOnboarding",
linksDisplay: "linksDisplay",
};

export function useWorkspaceStore<T>(
Expand Down
15 changes: 12 additions & 3 deletions apps/web/ui/links/link-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function LinkDisplay() {
setShowArchived,
displayProperties,
setDisplayProperties,
isLoading,
isDirty,
persist,
reset,
Expand All @@ -39,7 +40,7 @@ export default function LinkDisplay() {
const [openPopover, setOpenPopover] = useState(false);
const { queryParams } = useRouterStuff();

useKeyboardShortcut("a", () => setShowArchived((o) => !o));
useKeyboardShortcut("a", () => setShowArchived(!showArchived));

return (
<Popover
Expand Down Expand Up @@ -170,7 +171,14 @@ export default function LinkDisplay() {
className="h-8 w-auto px-2"
variant="primary"
text="Set as default"
onClick={persist}
onClick={() => {
if (
window.confirm(
"Set this configuration as the default for everyone in this workspace?",
)
)
persist();
}}
/>
</div>
</motion.div>
Expand All @@ -183,13 +191,14 @@ export default function LinkDisplay() {
>
<Button
variant="secondary"
disabled={isLoading}
className="hover:bg-white [&>div]:w-full"
textWrapperClassName="!overflow-visible"
text={
<div className="flex w-full items-center gap-2">
<div className="relative shrink-0">
<Sliders className="h-4 w-4" />
{isDirty && (
{isDirty && !isLoading && (
<div className="absolute -right-0.5 -top-0.5 size-2 rounded-full bg-blue-500">
<div className="h-full w-full animate-pulse rounded-full ring-2 ring-blue-500/40" />
</div>
Expand Down
125 changes: 61 additions & 64 deletions apps/web/ui/links/links-display-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { STORE_KEYS, useWorkspaceStore } from "@/lib/swr/use-workspace-store";
import { useLocalStorage } from "@dub/ui";
import { useSearchParams } from "next/navigation";
import {
Dispatch,
PropsWithChildren,
SetStateAction,
createContext,
useMemo,
useState,
} from "react";
import { PropsWithChildren, createContext, useMemo } from "react";

export const linkViewModes = ["cards", "rows"] as const;

Expand Down Expand Up @@ -65,6 +58,9 @@ export const linkDisplayProperties: {

export type LinkDisplayProperty = (typeof linkDisplayPropertyIds)[number];

const defaultViewMode = linkViewModes[0];
const defaultSortBy = sortOptions[0].slug;
const defaultShowArchived = false;
export const defaultDisplayProperties: LinkDisplayProperty[] = [
"icon",
"link",
Expand All @@ -75,37 +71,40 @@ export const defaultDisplayProperties: LinkDisplayProperty[] = [
"analytics",
];

type PersistedLinksDisplay = {
viewMode: LinksViewMode;
sortBy: LinksSortSlug;
showArchived: boolean;
displayProperties: LinkDisplayProperty[];
};

function useLinksDisplayOption<T>(
key: string,
parsePersisted: (value: T) => T,
defaultValue: T,
overrideValue?: T,
parseValue: (value: any) => T,
) {
const [valuePersisted, setValuePersisted] = useLocalStorage<T>(
const [value, setValue, { remove }] = useLocalStorage<T | undefined>(
`links-display-${key}`,
defaultValue,
undefined,
);
const [value, setValue] = useState(overrideValue ?? valuePersisted);

return {
value,
value: parseValue(value ?? defaultValue),
setValue,
valuePersisted,
setValuePersisted,
persist: () => setValuePersisted(value),
reset: () => setValue(parsePersisted(valuePersisted)),
reset: remove,
};
}

export const LinksDisplayContext = createContext<{
viewMode: LinksViewMode;
setViewMode: Dispatch<SetStateAction<LinksViewMode>>;
setViewMode: (mode: LinksViewMode) => void;
displayProperties: LinkDisplayProperty[];
setDisplayProperties: Dispatch<SetStateAction<LinkDisplayProperty[]>>;
setDisplayProperties: (properties: LinkDisplayProperty[]) => void;
sortBy: LinksSortSlug;
setSort: Dispatch<SetStateAction<LinksSortSlug>>;
setSort: (sort: LinksSortSlug) => void;
showArchived: boolean;
setShowArchived: Dispatch<SetStateAction<boolean>>;
setShowArchived: (show: boolean) => void;
isLoading: boolean;
isDirty: boolean;
persist: () => void;
reset: () => void;
Expand All @@ -118,6 +117,8 @@ export const LinksDisplayContext = createContext<{
setSort: () => {},
showArchived: false,
setShowArchived: () => {},
/** Whether the persisted values are being loaded */
isLoading: false,
/** Whether the current values differ from the persisted values */
isDirty: false,
/** Updates the persisted values to the current values */
Expand All @@ -134,94 +135,87 @@ const parseDisplayProperties = (displayPropertiesRaw: string[]) =>
(p) => displayPropertiesRaw.findIndex((pr) => pr === p) !== -1,
);

const parseSort = (sort: string) =>
sortOptions.find(({ slug }) => slug === sort)?.slug ?? sortOptions[0].slug;
const parseSortBy = (sortBy: string) =>
sortOptions.find(({ slug }) => slug === sortBy)?.slug ?? sortOptions[0].slug;

const parseShowArchived = (showArchived: boolean) => showArchived === true;

const parseObject = (object: any): PersistedLinksDisplay | undefined =>
object
? {
viewMode: parseViewMode(object.viewMode),
sortBy: parseSortBy(object.sortBy),
showArchived: parseShowArchived(object.showArchived),
displayProperties: parseDisplayProperties(object.displayProperties),
}
: undefined;

export function LinksDisplayProvider({ children }: PropsWithChildren) {
const searchParams = useSearchParams();
const sortRaw = searchParams?.get("sortBy");
const showArchivedRaw = searchParams?.get("showArchived");
steven-tey marked this conversation as resolved.
Show resolved Hide resolved
// Persisted values to workspace store
const [persistedRaw, setPersisted, { loading: isLoading }] =
useWorkspaceStore<PersistedLinksDisplay>(STORE_KEYS.linksDisplay);
const persisted = useMemo(() => parseObject(persistedRaw), [persistedRaw]);

// View mode
const {
value: viewMode,
setValue: setViewMode,
valuePersisted: viewModePersisted,
persist: persistViewMode,
reset: resetViewMode,
} = useLinksDisplayOption<string>(
} = useLinksDisplayOption<LinksViewMode>(
"view-mode",
persisted?.viewMode ?? defaultViewMode,
parseViewMode,
linkViewModes[0],
);

// Sort
const {
value: sortBy,
setValue: setSort,
valuePersisted: sortPersisted,
persist: persistSort,
reset: resetSort,
} = useLinksDisplayOption<string>(
} = useLinksDisplayOption<LinksSortSlug>(
"sortBy",
parseSort,
sortOptions[0].slug,
sortRaw ? parseSort(sortRaw) : undefined,
persisted?.sortBy ?? defaultSortBy,
parseSortBy,
);

// Show archived
const {
value: showArchived,
setValue: setShowArchived,
valuePersisted: showArchivedPersisted,
persist: persistShowArchived,
reset: resetShowArchived,
} = useLinksDisplayOption<boolean>(
"show-archived",
persisted?.showArchived ?? defaultShowArchived,
parseShowArchived,
false,
showArchivedRaw ? showArchivedRaw === "true" : undefined,
);

// Display properties
const {
value: displayProperties,
setValue: setDisplayProperties,
valuePersisted: displayPropertiesPersisted,
persist: persistDisplayProperties,
reset: resetDisplayProperties,
} = useLinksDisplayOption<LinkDisplayProperty[]>(
"display-properties",
persisted?.displayProperties ?? defaultDisplayProperties,
parseDisplayProperties,
defaultDisplayProperties,
);

const isDirty = useMemo(() => {
if (viewMode !== parseViewMode(viewModePersisted)) return true;
if (sortBy !== parseSort(sortPersisted)) return true;
if (showArchived !== parseShowArchived(showArchivedPersisted)) return true;
if (viewMode !== (persisted?.viewMode ?? defaultViewMode)) return true;
if (sortBy !== (persisted?.sortBy ?? defaultSortBy)) return true;
if (showArchived !== (persisted?.showArchived ?? defaultShowArchived))
return true;
if (
displayProperties.slice().sort().join(",") !==
parseDisplayProperties(displayPropertiesPersisted)
(persisted?.displayProperties ?? defaultDisplayProperties)
.slice()
.sort()
.join(",")
)
return true;

return false;
}, [
viewModePersisted,
viewMode,
sortPersisted,
sortBy,
showArchivedPersisted,
showArchived,
displayPropertiesPersisted,
displayProperties,
]);
}, [persisted, viewMode, sortBy, showArchived, displayProperties]);

return (
<LinksDisplayContext.Provider
Expand All @@ -234,18 +228,21 @@ export function LinksDisplayProvider({ children }: PropsWithChildren) {
setSort,
showArchived,
setShowArchived,
isLoading,
isDirty,
persist: () => {
persistViewMode();
persistDisplayProperties();
persistSort();
persistShowArchived();
setPersisted({
viewMode: viewMode,
sortBy: sortBy,
showArchived: showArchived,
displayProperties: displayProperties,
});
},
reset: () => {
resetViewMode();
resetDisplayProperties();
resetSort();
resetShowArchived();
resetDisplayProperties();
},
}}
>
Expand Down
13 changes: 11 additions & 2 deletions packages/ui/src/hooks/use-local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function getItemFromLocalStorage(key: string) {
export function useLocalStorage<T>(
key: string,
initialValue: T,
): [T, (value: T) => void] {
): [T, (value: T) => void, { remove: () => void }] {
const [storedValue, setStoredValue] = useState(
getItemFromLocalStorage(key) ?? initialValue,
);
Expand All @@ -30,5 +30,14 @@ export function useLocalStorage<T>(
window.localStorage.setItem(key, JSON.stringify(value));
};

return [storedValue, setValue];
return [
storedValue,
setValue,
{
remove: () => {
setStoredValue(initialValue);
window.localStorage.removeItem(key);
},
},
];
}
Loading