WIP: move item queries to react-query to better handle mutations
This commit is contained in:
155
packages/web/lib/networking/queries/gql-queries.tsx
Normal file
155
packages/web/lib/networking/queries/gql-queries.tsx
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
138
packages/web/lib/networking/queries/types.tsx
Normal file
138
packages/web/lib/networking/queries/types.tsx
Normal 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
|
||||
}
|
||||
322
packages/web/lib/networking/queries/useLibraryItems.tsx
Normal file
322
packages/web/lib/networking/queries/useLibraryItems.tsx
Normal 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,
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user