WIP: move item queries to react-query to better handle mutations

This commit is contained in:
Jackson Harper
2024-07-26 23:44:36 +08:00
parent c529e52936
commit 06af855621
3 changed files with 615 additions and 0 deletions

View File

@ -0,0 +1,155 @@
import { gql } from 'graphql-request'
import { highlightFragment } from '../fragments/highlightFragment'
export const recommendationFragment = gql`
fragment RecommendationFields on Recommendation {
id
name
note
user {
userId
name
username
profileImageURL
}
recommendedAt
}
`
export const GQL_SEARCH_QUERY = gql`
query Search(
$after: String
$first: Int
$query: String
$includeContent: Boolean
) {
search(
first: $first
after: $after
query: $query
includeContent: $includeContent
) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
folder
pageType
contentReader
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
author
image
description
publishedAt
ownedByViewer
originalArticleUrl
uploadFileId
labels {
id
name
color
}
pageId
shortId
quote
annotation
state
siteName
siteIcon
subscription
readAt
savedAt
wordsCount
recommendations {
id
name
note
user {
userId
name
username
profileImageURL
}
recommendedAt
}
highlights {
...HighlightFields
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
${highlightFragment}
`
export const GQL_SET_LINK_ARCHIVED = gql`
mutation SetLinkArchived($input: ArchiveLinkInput!) {
setLinkArchived(input: $input) {
... on ArchiveLinkSuccess {
linkId
message
}
... on ArchiveLinkError {
message
errorCodes
}
}
}
`
export const GQL_DELETE_LIBRARY_ITEM = gql`
mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) {
setBookmarkArticle(input: $input) {
... on SetBookmarkArticleSuccess {
bookmarkedArticle {
id
}
}
... on SetBookmarkArticleError {
errorCodes
}
}
}
`
export const GQL_UPDATE_LIBRARY_ITEM = gql`
mutation UpdatePage($input: UpdatePageInput!) {
updatePage(input: $input) {
... on UpdatePageSuccess {
updatedPage {
id
title
url
createdAt
author
image
description
savedAt
publishedAt
}
}
... on UpdatePageError {
errorCodes
}
}
}
`

View File

@ -0,0 +1,138 @@
import { State } from '../fragments/articleFragment'
export interface ReadableItem {
id: string
title: string
slug: string
}
export type LibraryItemsQueryInput = {
limit: number
sortDescending: boolean
searchQuery?: string
cursor?: string
includeContent?: boolean
}
export type LibraryItemsData = {
search: LibraryItems
errorCodes?: string[]
}
export type LibraryItems = {
edges: LibraryItem[]
pageInfo: PageInfo
errorCodes?: string[]
}
export type LibraryItem = {
cursor: string
node: LibraryItemNode
isLoading?: boolean | undefined
}
export type LibraryItemNode = {
id: string
title: string
url: string
author?: string
image?: string
createdAt: string
publishedAt?: string
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
folder?: string
isArchived: boolean
description: string
ownedByViewer: boolean
uploadFileId: string
labels?: Label[]
pageId: string
shortId: string
quote: string
annotation: string
state: State
pageType: PageType
siteName?: string
siteIcon?: string
subscription?: string
readAt?: string
savedAt?: string
wordsCount?: number
aiSummary?: string
recommendations?: Recommendation[]
highlights?: Highlight[]
}
export type Recommendation = {
id: string
name: string
note?: string
user?: RecommendingUser
recommendedAt: Date
}
export type RecommendingUser = {
userId: string
name: string
username: string
profileImageURL?: string
}
export type PageInfo = {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
totalCount: number
}
export type SetLinkArchivedInput = {
linkId: string
archived: boolean
}
type SetLinkArchivedSuccess = {
linkId: string
message?: string
}
export type SetLinkArchivedData = {
setLinkArchived: SetLinkArchivedSuccess
errorCodes?: string[]
}
export type DeleteItemInput = {
articleID: string
bookmark: boolean
}
export type SetBookmarkArticle = {
errorCodes?: string[]
}
export type SetBookmarkArticleData = {
setBookmarkArticle: SetBookmarkArticle
}
export type UpdateLibraryItemInput = {
pageId: string
title?: string
byline?: string | undefined
description?: string
savedAt?: string
publishedAt?: string
state?: State
}
export type UpdateLibraryItem = {
errorCodes?: string[]
}
export type UpdateLibraryItemData = {
updatePage: UpdateLibraryItem
}

View File

@ -0,0 +1,322 @@
import { gql, GraphQLClient } from 'graphql-request'
import {
InfiniteData,
QueryClient,
useInfiniteQuery,
useMutation,
useQueryClient,
} from 'react-query'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../toastHelpers'
import { ContentReader, PageType, State } from '../fragments/articleFragment'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { updatePageMutation } from '../mutations/updatePageMutation'
import { makeGqlFetcher, requestHeaders } from '../networkHelpers'
import { Label } from '../fragments/labelFragment'
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
import {
LibraryItemNode,
LibraryItems,
LibraryItemsData,
LibraryItemsQueryInput,
SetBookmarkArticleData,
SetLinkArchivedData,
SetLinkArchivedInput,
UpdateLibraryItemData,
} from './types'
import {
GQL_DELETE_LIBRARY_ITEM,
GQL_SEARCH_QUERY,
GQL_SET_LINK_ARCHIVED,
GQL_UPDATE_LIBRARY_ITEM,
} from './gql-queries'
import { parseGraphQLResponse } from './gql-errors'
import { gqlEndpoint } from '../../appConfig'
import { GraphQLResponse } from 'graphql-request/dist/types'
function gqlFetcher(
query: string,
variables?: unknown,
requiresAuth = true
): Promise<unknown> {
// if (requiresAuth) {
// verifyAuth()
// }
const graphQLClient = new GraphQLClient(gqlEndpoint, {
credentials: 'include',
mode: 'cors',
})
return graphQLClient.request(query, variables, requestHeaders())
}
const updateItemStateInCache = (
queryClient: QueryClient,
itemId: string,
newState: State
) => {
const keys = queryClient.getQueryCache().findAll('libraryItems')
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: any) => {
if (!data) return data
return {
...data,
pages: data.pages.map((page: any) => ({
...page,
edges: page.edges.map((edge: any) =>
edge.node.id === itemId
? { ...edge, node: { ...edge.node, state: newState } }
: edge
),
})),
}
})
})
}
export function useGetLibraryItems(
folder: string | undefined,
{ limit, searchQuery }: LibraryItemsQueryInput
) {
const fullQuery = folder
? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
: searchQuery ?? ''
return useInfiniteQuery(
['libraryItems', fullQuery],
async ({ pageParam }) => {
const response = (await gqlFetcher(GQL_SEARCH_QUERY, {
after: pageParam,
first: limit,
query: fullQuery,
includeContent: false,
})) as LibraryItemsData
return response.search
},
{
getNextPageParam: (lastPage: LibraryItems) => {
return lastPage.pageInfo.hasNextPage
? lastPage?.pageInfo?.endCursor
: undefined
},
}
)
}
export const useArchiveItem = () => {
const queryClient = useQueryClient()
const archiveItem = async (input: SetLinkArchivedInput) => {
const result = (await gqlFetcher(GQL_SET_LINK_ARCHIVED, {
input,
})) as SetLinkArchivedData
if (result.errorCodes?.length) {
throw new Error(result.errorCodes[0])
}
return result.setLinkArchived
}
return useMutation(archiveItem, {
onMutate: async (input: SetLinkArchivedInput) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(
queryClient,
input.linkId,
input.archived ? State.ARCHIVED : State.SUCCEEDED
)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
export const useDeleteItem = () => {
const queryClient = useQueryClient()
const deleteItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_DELETE_LIBRARY_ITEM, {
input: { articleID: itemId, bookmark: false },
})) as SetBookmarkArticleData
if (result.setBookmarkArticle.errorCodes?.length) {
throw new Error(result.setBookmarkArticle.errorCodes[0])
}
return result.setBookmarkArticle
}
return useMutation(deleteItem, {
onMutate: async (itemId: string) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(queryClient, itemId, State.DELETED)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
export const useRestoreItem = () => {
const queryClient = useQueryClient()
const restoreItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_UPDATE_LIBRARY_ITEM, {
input: { pageId: itemId, state: State.SUCCEEDED },
})) as UpdateLibraryItemData
console.log('result: ', result)
if (result.updatePage.errorCodes?.length) {
throw new Error(result.updatePage.errorCodes[0])
}
return result.updateLibraryItem
}
return useMutation(restoreItem, {
onMutate: async (itemId: string) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(queryClient, itemId, State.SUCCEEDED)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
// export const useRestoreItem = () => {
// const queryClient = useQueryClient()
// return useMutation(restoreItem, {
// onMutate: async (itemId) => {
// await queryClient.cancelQueries('libraryItems')
// const previousItems = queryClient.getQueryData<any>('libraryItems')
// updateItemStateInCache(queryClient, itemId, 'ACTIVE')
// return { previousItems }
// },
// onError: (error, itemId, context) => {
// if (context?.previousItems) {
// queryClient.setQueryData('libraryItems', context.previousItems)
// }
// },
// onSettled: () => {
// queryClient.invalidateQueries('libraryItems')
// },
// })
// }
export function useGetRawSearchItemsQuery(
{
limit,
searchQuery,
cursor,
includeContent = false,
}: LibraryItemsQueryInput,
shouldFetch = true
) {
// const query = gql`
// query Search(
// $after: String
// $first: Int
// $query: String
// $includeContent: Boolean
// ) {
// search(
// first: $first
// after: $after
// query: $query
// includeContent: $includeContent
// ) {
// ... on SearchSuccess {
// edges {
// cursor
// node {
// id
// title
// slug
// url
// folder
// createdAt
// author
// image
// description
// publishedAt
// originalArticleUrl
// siteName
// siteIcon
// subscription
// readAt
// savedAt
// wordsCount
// }
// }
// pageInfo {
// hasNextPage
// hasPreviousPage
// startCursor
// endCursor
// totalCount
// }
// }
// ... on SearchError {
// errorCodes
// }
// }
// }
// `
// const { data, error, isFetching, refetch } = useQuery(
// ['rawSearchItems', searchQuery, cursor],
// () =>
// makeGqlFetcher(query, {
// after: cursor,
// first: limit,
// query: searchQuery,
// includeContent,
// }),
// {
// enabled: shouldFetch,
// refetchOnWindowFocus: false,
// }
// )
// const responseData = data as LibraryItemsData | undefined
// if (responseData?.errorCodes) {
return {
isFetching: false,
items: [],
isLoading: false,
error: true,
}
// }
// return {
// isFetching,
// items: responseData?.search.edges.map((edge) => edge.node) ?? [],
// itemsDataError: error,
// isLoading: !error && !data,
// error: !!error,
// }
}