Use react-query for shortcuts

This commit is contained in:
Jackson Harper
2024-07-30 22:15:38 +08:00
parent 9466a778e1
commit 11e336735f
3 changed files with 142 additions and 92 deletions

View File

@ -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 (
<VStack
css={{

View File

@ -26,8 +26,6 @@ import { NavigationSection } from '../NavigationLayout'
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
import { ListMagnifyingGlass } from '@phosphor-icons/react'
import React from 'react'
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import { fetchEndpoint } from '../../../lib/appConfig'
import { requestHeaders } from '../../../lib/networking/networkHelpers'
import { v4 as uuidv4 } from 'uuid'
@ -40,6 +38,11 @@ import { ShortcutFolderClosed } from '../../elements/icons/ShortcutFolderClosed'
import { TrashSectionIcon } from '../../elements/icons/TrashSectionIcon'
import { ShortcutFolderOpen } from '../../elements/icons/ShortcutFolderOpen'
import useResizeObserver from 'use-resize-observer'
import {
useGetShortcuts,
useResetShortcuts,
useSetShortcuts,
} from '../../../lib/networking/shortcuts/useShortcuts'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -268,10 +271,7 @@ const LibraryNav = (props: NavigationMenuProps): JSX.Element => {
const Shortcuts = (props: NavigationMenuProps): JSX.Element => {
const treeRef = useRef<TreeApi<Shortcut> | 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<string, boolean>
>({
@ -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 && (
<Tree
ref={props.treeRef}
data={data as Shortcut[]}

View File

@ -0,0 +1,117 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestHeaders } from '../networkHelpers'
import { fetchEndpoint } from '../../appConfig'
import { Shortcut } from '../../../components/templates/navMenu/NavigationMenu'
export function useGetShortcuts() {
return useQuery({
queryKey: ['shortcuts'],
queryFn: async () => {
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<Shortcut[]> {
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<Shortcut[]> {
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<Shortcut[]> {
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[]
}