From 11e336735f795055a6ce66da0ffa26817f97b5ae Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 30 Jul 2024 22:15:38 +0800 Subject: [PATCH] Use react-query for shortcuts --- .../templates/navMenu/LibraryMenu.tsx | 54 -------- .../templates/navMenu/NavigationMenu.tsx | 63 ++++------ .../lib/networking/shortcuts/useShortcuts.tsx | 117 ++++++++++++++++++ 3 files changed, 142 insertions(+), 92 deletions(-) create mode 100644 packages/web/lib/networking/shortcuts/useShortcuts.tsx diff --git a/packages/web/components/templates/navMenu/LibraryMenu.tsx b/packages/web/components/templates/navMenu/LibraryMenu.tsx index 6e4849bba..aa8313794 100644 --- a/packages/web/components/templates/navMenu/LibraryMenu.tsx +++ b/packages/web/components/templates/navMenu/LibraryMenu.tsx @@ -6,15 +6,12 @@ import { Circle, DotsThree, MagnifyingGlass, X } from '@phosphor-icons/react' import { Subscription, SubscriptionType, - useGetSubscriptionsQuery, } from '../../../lib/networking/queries/useGetSubscriptionsQuery' -import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' import { Label } from '../../../lib/networking/fragments/labelFragment' import { theme } from '../../tokens/stitches.config' import { useRegisterActions } from 'kbar' import { LogoBox } from '../../elements/LogoBox' import { usePersistedState } from '../../../lib/hooks/usePersistedState' -import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery' import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment' import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon' import Link from 'next/link' @@ -185,57 +182,6 @@ const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => { initialValue: [], }) - // const shortcuts: Shortcut[] = [ - // { - // id: '12asdfasdf', - // name: 'Omnivore Blog', - // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'subscription:"Money Talk"', - // type: 'feed', - // }, - // { - // id: 'sdfsdfgdsfg', - // name: 'Follow the Money | Arne & Harr', - // filter: 'subscription:"Money Talk"', - // type: 'feed', - // }, - // { - // id: 'sdfasdfasdfsdfsdfsgasdfg', - // name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'newsletter', - // }, - // { - // id: 'sdfasdfasdfsdfsdfsgasdfg', - // name: 'Robert的博客', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'feed', - // }, - // { - // id: 'sdfasdfasdfasdfasf', - // name: 'Oldest First', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'search', - // }, - // { - // id: 'sdfasdfasdfgasdfg', - // name: 'Hockey', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'label', - // label: { - // id: 'sdfsdfsdf', - // name: 'Hockey', - // color: '#E98B8B', - // createdAt: new Date(), - // }, - // }, - // ] - // - return ( { const Shortcuts = (props: NavigationMenuProps): JSX.Element => { const treeRef = useRef | undefined>(undefined) - const { trigger: resetShortcutsTrigger } = useSWRMutation( - '/api/shortcuts', - resetShortcuts - ) + const resetShortcuts = useResetShortcuts() const createNewFolder = useCallback(async () => { if (treeRef.current) { @@ -283,9 +283,7 @@ const Shortcuts = (props: NavigationMenuProps): JSX.Element => { }, [treeRef]) const resetShortcutsToDefault = useCallback(async () => { - resetShortcutsTrigger(null, { - revalidate: true, - }) + await resetShortcuts.mutateAsync() }, []) return ( @@ -439,15 +437,10 @@ const cachedShortcutsData = (): Shortcut[] | undefined => { const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => { const router = useRouter() const { ref, width, height } = useResizeObserver() + const { data, isLoading } = useGetShortcuts() + const setShorcuts = useSetShortcuts() + const resetShortcuts = useResetShortcuts() - const { isValidating, data } = useSWR('/api/shortcuts', getShortcuts, { - revalidateOnFocus: false, - fallbackData: cachedShortcutsData(), - onSuccess(data) { - localStorage.setItem('/api/shortcuts', JSON.stringify(data)) - }, - }) - const { trigger, isMutating } = useSWRMutation('/api/shortcuts', setShortcuts) const [folderOpenState, setFolderOpenState] = usePersistedState< Record >({ @@ -460,55 +453,49 @@ const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => { return result }, [data]) - const syncTreeData = (data: Shortcut[]) => { - trigger( - { shortcuts: data }, - { - optimisticData: data, - rollbackOnError: true, - populateCache: (updatedShortcuts) => { - return updatedShortcuts - }, - revalidate: false, - } - ) + const syncTreeData = async (data: Shortcut[]) => { + await setShorcuts.mutateAsync({ shortcuts: data }) } const onMove = useCallback( - (args: { dragIds: string[]; parentId: null | string; index: number }) => { + async (args: { + dragIds: string[] + parentId: null | string + index: number + }) => { for (const id of args.dragIds) { tree?.move({ id, parentId: args.parentId, index: args.index }) } - syncTreeData(tree.data) + await syncTreeData(tree.data) }, [tree, data] ) const onCreate = useCallback( - (args: { parentId: string | null; index: number; type: string }) => { + async (args: { parentId: string | null; index: number; type: string }) => { const data = { id: uuidv4(), name: '', type: 'folder' } as any if (args.type === 'internal') { data.children = [] } tree.create({ parentId: args.parentId, index: args.index, data }) - syncTreeData(tree.data) + await syncTreeData(tree.data) return data }, [tree, data] ) const onDelete = useCallback( - (args: { ids: string[] }) => { + async (args: { ids: string[] }) => { args.ids.forEach((id) => tree.drop({ id })) - syncTreeData(tree.data) + await syncTreeData(tree.data) }, [tree, data] ) const onRename = useCallback( - (args: { name: string; id: string }) => { + async (args: { name: string; id: string }) => { tree.update({ id: args.id, changes: { name: args.name } as any }) - syncTreeData(tree.data) + await syncTreeData(tree.data) }, [tree, data] ) @@ -575,7 +562,7 @@ const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => { minBlockSize: 0, }} > - {!isValidating && ( + {!isLoading && ( { + return await getShortcuts() + }, + }) +} + +export const useSetShortcuts = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { shortcuts: Shortcut[] }) => { + return await setShortcuts(variables) + }, + onMutate: async (variables: { shortcuts: Shortcut[] }) => { + await queryClient.cancelQueries({ queryKey: ['shortcuts'] }) + queryClient.setQueryData(['shortcuts'], variables.shortcuts) + const previousState = { + previousItems: queryClient.getQueryData(['shortcuts']), + } + return previousState + }, + onError: (error, variables, context) => { + if (context?.previousItems) { + queryClient.setQueryData(['shortcuts'], context.previousItems) + } + }, + }) +} + +export const useResetShortcuts = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async () => { + return await resetShortcuts() + }, + onMutate: async () => { + const previousState = { + previousItems: queryClient.getQueryData(['shortcuts']), + } + return previousState + }, + onError: (error, variables, context) => { + if (context?.previousItems) { + queryClient.setQueryData(['shortcuts'], context.previousItems) + } + }, + onSuccess: (data, variables, context) => { + queryClient.setQueryData(['shortcuts'], data) + }, + }) +} + +async function getShortcuts(): Promise { + const url = new URL(`/api/shortcuts`, fetchEndpoint) + try { + const response = await fetch(url.toString(), { + method: 'GET', + headers: requestHeaders(), + credentials: 'include', + mode: 'cors', + }) + const payload = await response.json() + if ('shortcuts' in payload) { + return payload['shortcuts'] as Shortcut[] + } + return [] + } catch (err) { + console.log('error getting shortcuts: ', err) + throw err + } +} + +async function setShortcuts(variables: { + shortcuts: Shortcut[] +}): Promise { + const url = new URL(`/api/shortcuts`, fetchEndpoint) + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...requestHeaders(), + }, + credentials: 'include', + mode: 'cors', + body: JSON.stringify({ shortcuts: variables.shortcuts }), + }) + const payload = await response.json() + if (!('shortcuts' in payload)) { + throw new Error('Error syncing shortcuts') + } + return payload['shortcuts'] as Shortcut[] +} + +async function resetShortcuts(): Promise { + const url = new URL(`/api/shortcuts`, fetchEndpoint) + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...requestHeaders(), + }, + credentials: 'include', + mode: 'cors', + }) + const payload = await response.json() + if (!('shortcuts' in payload)) { + throw new Error('Error syncing shortcuts') + } + return payload['shortcuts'] as Shortcut[] +}