Migrate to react-query for the saved searches

This commit is contained in:
Jackson Harper
2024-07-31 14:59:06 +08:00
parent 1fc73bd59e
commit d44f5cc6ff
13 changed files with 296 additions and 273 deletions

View File

@ -13,7 +13,6 @@ 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'
@ -21,6 +20,7 @@ import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { NavMenuFooter } from './Footer'
import { escapeQuotes } from '../../../utils/helper'
import { useGetLabels } from '../../../lib/networking/labels/useLabels'
import { useGetSavedSearches } from '../../../lib/networking/savedsearches/useSavedSearches'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -51,7 +51,7 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
initialValue: [],
})
const labelsResponse = useGetLabels()
const searchesResponse = useGetSavedSearchQuery()
const searchesResponse = useGetSavedSearches()
const subscriptionsResponse = useGetSubscriptionsQuery()
useEffect(() => {
@ -78,9 +78,9 @@ export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
if (
!searchesResponse.error &&
!searchesResponse.isLoading &&
searchesResponse.savedSearches
searchesResponse?.data
) {
setSavedSearches(searchesResponse.savedSearches)
setSavedSearches(searchesResponse.data)
}
}, [setSavedSearches, searchesResponse])

View File

@ -105,7 +105,6 @@ export const useUpdateLabel = () => {
return useMutation({
mutationFn: updateLabel,
onSuccess: (updatedLabel) => {
console.log('updated label: ', updatedLabel)
if (updatedLabel) {
const keys = queryClient
.getQueryCache()

View File

@ -1,47 +0,0 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
export enum ReminderType {
Tonight = 'TONIGHT',
Tomorrow = 'TOMORROW',
ThisWeekend = 'THIS_WEEKEND',
NextWeek = 'NEXT_WEEK',
}
export async function createReminderMutation(
linkId: string,
reminderType: ReminderType,
archiveUntil: boolean,
sendNotification: boolean
): Promise<string | undefined> {
const mutation = gql`
mutation createReminderMutation($input: CreateReminderInput!) {
createReminder(input: $input) {
... on CreateReminderSuccess {
reminder {
id
remindAt
}
}
... on CreateReminderError {
errorCodes
}
}
}
`
try {
const input = {
linkId,
reminderType,
archiveUntil,
sendNotification,
scheduledAt: new Date(),
}
const data = await gqlFetcher(mutation, { input })
return 'data'
} catch (error) {
console.log('createReminder error', error)
return undefined
}
}

View File

@ -1,36 +0,0 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { SavedSearch } from "../fragments/savedSearchFragment"
export type DeleteFilterInput = string
type DeleteFilterOutput = {
deleteFilter: { filter: SavedSearch }
}
export async function deleteFilterMutation (
id: DeleteFilterInput
): Promise<SavedSearch | undefined> {
const mutation = gql`
mutation DeleteFilter($id: ID!) {
deleteFilter(id: $id) {
... on DeleteFilterSuccess {
filter {
id
}
}
... on DeleteFilterError {
errorCodes
}
}
}
`
try {
const data = await gqlFetcher(mutation, { id })
const output = data as DeleteFilterOutput | undefined
return output?.deleteFilter.filter
} catch {
return undefined
}
}

View File

@ -1,46 +0,0 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { SavedSearch } from "../fragments/savedSearchFragment"
export type AddFilterInput = {
name: string
filter: string
category: string
position: number
folder?: string
}
type AddFilterOutput = {
saveFilter: { filter: SavedSearch }
}
export async function saveFilterMutation (
input: AddFilterInput
): Promise<SavedSearch | undefined> {
const mutation = gql`
mutation SaveFilter($input: SaveFilterInput!) {
saveFilter(input: $input) {
... on SaveFilterSuccess {
filter {
id
name
filter
position
visible
defaultFilter
folder
category
}
}
... on SaveFilterError {
errorCodes
}
}
}
`
const data = await gqlFetcher(mutation, { input })
const output = data as AddFilterOutput | undefined
return output?.saveFilter.filter
}

View File

@ -1,47 +0,0 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { SavedSearch } from "../fragments/savedSearchFragment"
export type UpdateFilterInput = {
id?: string
name?: string
filter?: string
position?: number
category?: string
description?: string
visible?: boolean
folder?: string
}
type UpdateFilterOutput = {
filter: SavedSearch
}
export async function updateFilterMutation (
input: UpdateFilterInput
): Promise<string | undefined> {
const mutation = gql`
mutation UpdateFilter($input: UpdateFilterInput!) {
updateFilter(input: $input) {
... on UpdateFilterSuccess {
filter {
id
}
}
... on UpdateFilterError {
errorCodes
}
}
}
`
try {
const { id, name, visible, filter, position } = input
const data = await gqlFetcher(mutation, { input: {id, name, filter, position, visible }})
const output = data as UpdateFilterOutput | undefined
return output?.filter?.id
} catch {
return undefined
}
}

View File

@ -1,6 +1,6 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { gqlFetcher, makeGqlFetcher, publicGqlFetcher } from '../networkHelpers'
import { makeGqlFetcher } from '../networkHelpers'
type HomeResult = {
home: {

View File

@ -1,56 +0,0 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { publicGqlFetcher } from '../networkHelpers'
import {
SavedSearch,
savedSearchFragment,
} from '../fragments/savedSearchFragment'
type SavedSearchResponse = {
error: any
savedSearches?: SavedSearch[]
savedSearchErrors?: unknown
isLoading: boolean
}
type SavedSearchResponseData = {
filters: { filters: SavedSearch[] }
}
export function useGetSavedSearchQuery(): SavedSearchResponse {
const query = gql`
query SavedSearches {
filters {
... on FiltersSuccess {
filters {
...FiltersFragment
}
}
... on FiltersError {
errorCodes
}
}
}
${savedSearchFragment}
`
const { data, error } = useSWR(query, publicGqlFetcher)
if (data) {
const { filters } = data as SavedSearchResponseData
return {
error,
savedSearches: filters?.filters ?? [],
savedSearchErrors: error ?? {},
isLoading: false,
}
}
return {
error,
savedSearches: [],
savedSearchErrors: null,
isLoading: !error && !data,
}
}

View File

@ -0,0 +1,68 @@
import { gql } from 'graphql-request'
import { savedSearchFragment } from '../fragments/savedSearchFragment'
export const GQL_GET_SAVED_SEARCHES = gql`
query SavedSearches {
filters {
... on FiltersSuccess {
filters {
...FiltersFragment
}
}
... on FiltersError {
errorCodes
}
}
}
${savedSearchFragment}
`
export const GQL_DELETE_SAVED_SEARCH = gql`
mutation DeleteFilter($id: ID!) {
deleteFilter(id: $id) {
... on DeleteFilterSuccess {
filter {
...FiltersFragment
}
}
... on DeleteFilterError {
errorCodes
}
}
}
${savedSearchFragment}
`
export const GQL_CREATE_SAVED_SEARCH = gql`
mutation SaveFilter($input: SaveFilterInput!) {
saveFilter(input: $input) {
... on SaveFilterSuccess {
filter {
...FiltersFragment
}
}
... on SaveFilterError {
errorCodes
}
}
}
${savedSearchFragment}
`
export const GQL_UPDATE_SAVED_SEARCH = gql`
mutation UpdateFilter($input: UpdateFilterInput!) {
updateFilter(input: $input) {
... on UpdateFilterSuccess {
filter {
...FiltersFragment
}
}
... on UpdateFilterError {
errorCodes
}
}
}
${savedSearchFragment}
`

View File

@ -0,0 +1,173 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { gqlFetcher } from '../networkHelpers'
import {
GQL_CREATE_SAVED_SEARCH,
GQL_DELETE_SAVED_SEARCH,
GQL_GET_SAVED_SEARCHES,
GQL_UPDATE_SAVED_SEARCH,
} from './gql'
import { SavedSearch } from '../fragments/savedSearchFragment'
export function useGetSavedSearches() {
return useQuery({
queryKey: ['filters'],
queryFn: async () => {
const response = (await gqlFetcher(
GQL_GET_SAVED_SEARCHES
)) as SavedSearchData
if (response.filters?.errorCodes?.length) {
throw new Error(response.filters.errorCodes[0])
}
return response.filters.filters
},
})
}
export const useCreateSavedSearch = () => {
const queryClient = useQueryClient()
const createSavedSearch = async (variables: {
name: string
filter: string
category: string
position: number
}) => {
const result = (await gqlFetcher(GQL_CREATE_SAVED_SEARCH, {
input: {
name: variables.name,
filter: variables.filter,
category: variables.category,
position: variables.position,
},
})) as CreateSavedSearchData
if (result.saveFilter.errorCodes?.length) {
throw new Error(result.saveFilter.errorCodes[0])
}
return result.saveFilter?.filter
}
return useMutation({
mutationFn: createSavedSearch,
onSuccess: (newSavedSearch) => {
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['filters'] })
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
return [...data, newSavedSearch]
})
})
},
})
}
export const useUpdateSavedSearch = () => {
const queryClient = useQueryClient()
const updateSavedSearch = async (variables: {
input: UpdateSavedSearchInput
}) => {
const result = (await gqlFetcher(GQL_UPDATE_SAVED_SEARCH, {
input: {
id: variables.input.id,
name: variables.input.name,
visible: variables.input.visible,
filter: variables.input.filter,
position: variables.input.position,
},
})) as UpdateSavedSearchData
if (result.updateFilter.errorCodes?.length) {
throw new Error(result.updateFilter.errorCodes[0])
}
return result.updateFilter?.filter
}
return useMutation({
mutationFn: updateSavedSearch,
onSuccess: (updatedSavedSearch) => {
if (updatedSavedSearch) {
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['filters'] })
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
return [
...data.filter(
(savedSearch) => savedSearch.id !== updatedSavedSearch.id
),
updatedSavedSearch,
]
})
})
}
},
})
}
export const useDeleteSavedSearch = () => {
const queryClient = useQueryClient()
const deleteSavedSearch = async (variables: { searchId: string }) => {
const result = (await gqlFetcher(GQL_DELETE_SAVED_SEARCH, {
id: variables.searchId,
})) as DeleteSavedSearchData
if (result.deleteFilter.errorCodes?.length) {
throw new Error(result.deleteFilter.errorCodes[0])
}
return result.deleteFilter?.filter?.id
}
return useMutation({
mutationFn: deleteSavedSearch,
onSuccess: (deletedId) => {
if (deletedId) {
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['filters'] })
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: SavedSearch[]) => {
return data.filter((filter) => filter.id !== deletedId)
})
})
}
},
})
}
type UpdateSavedSearchResult = {
filter?: SavedSearch
errorCodes?: string[]
}
type UpdateSavedSearchData = {
updateFilter: UpdateSavedSearchResult
}
type CreateSavedSearchResult = {
filter?: SavedSearch
errorCodes?: string[]
}
type CreateSavedSearchData = {
saveFilter: CreateSavedSearchResult
}
type DeleteSavedSearchResult = {
filter?: SavedSearch
errorCodes?: string[]
}
type DeleteSavedSearchData = {
deleteFilter: DeleteSavedSearchResult
}
type FiltersResult = {
filters?: SavedSearch[]
errorCodes?: string[]
}
type SavedSearchData = {
filters: FiltersResult
}
export type UpdateSavedSearchInput = {
id?: string
name?: string
filter?: string
position?: number
category?: string
description?: string
visible?: boolean
folder?: string
}

View File

@ -15,13 +15,13 @@ import {
import { StyledText } from '../../components/elements/StyledText'
import { SettingsLayout } from '../../components/templates/SettingsLayout'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
import { Label } from '../../lib/networking/fragments/labelFragment'
import { CheckSquare, Circle, Square } from '@phosphor-icons/react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
import { escapeQuotes } from '../../utils/helper'
import { useGetLabels } from '../../lib/networking/labels/useLabels'
import { useGetSavedSearches } from '../../lib/networking/savedsearches/useSavedSearches'
export type PinnedSearch = {
type: 'saved-search' | 'label'
@ -35,7 +35,7 @@ type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
export default function PinnedSearches(): JSX.Element {
const { data: labels } = useGetLabels()
const { savedSearches } = useGetSavedSearchQuery()
const { data: savedSearches } = useGetSavedSearches()
const [hidePinnedSearches, setHidePinnedSearches] = usePersistedState({
key: '--library-hide-pinned-searches',
initialValue: false,

View File

@ -32,13 +32,15 @@ import {
} from '../../components/elements/DropdownElements'
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
import { InfoLink } from '../../components/elements/InfoLink'
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import CheckboxComponent from '../../components/elements/Checkbox'
import { updateFilterMutation } from '../../lib/networking/mutations/updateFilterMutation'
import { saveFilterMutation } from '../../lib/networking/mutations/saveFilterMutation'
import { inRange } from 'lodash'
import { deleteFilterMutation } from '../../lib/networking/mutations/deleteFilterMutation'
import {
useCreateSavedSearch,
useDeleteSavedSearch,
useGetSavedSearches,
useUpdateSavedSearch,
} from '../../lib/networking/savedsearches/useSavedSearches'
const HeaderWrapper = styled(Box, {
width: '100%',
@ -150,7 +152,11 @@ const Input = styled('input', { ...inputStyles })
const TextArea = styled('textarea', { ...inputStyles })
export default function SavedSearchesPage(): JSX.Element {
const { savedSearches, isLoading } = useGetSavedSearchQuery()
const { data: savedSearches, isLoading } = useGetSavedSearches()
const deleteSavedSearch = useDeleteSavedSearch()
const createSavedSearch = useCreateSavedSearch()
const updateSavedSearch = useUpdateSavedSearch()
const [nameInputText, setNameInputText] = useState<string>('')
const [queryInputText, setQueryInputText] = useState<string>('')
const [editingId, setEditingId] = useState<string | null>(null)
@ -194,9 +200,9 @@ export default function SavedSearchesPage(): JSX.Element {
setEditingId(null)
}
async function createSavedSearch(): Promise<void> {
async function doCreateSavedSearch(): Promise<void> {
try {
const savedFilter = await saveFilterMutation({
const savedFilter = await createSavedSearch.mutateAsync({
name: nameInputText,
filter: queryInputText,
category: 'Search',
@ -213,7 +219,7 @@ export default function SavedSearchesPage(): JSX.Element {
}
}
async function updateSavedSearch(id: string): Promise<void> {
async function doUpdateSavedSearch(id: string): Promise<void> {
resetSavedSearchState()
const changedSortedSearch = sortedSavedSearch?.find((it) => it.id == id)
@ -221,10 +227,12 @@ export default function SavedSearchesPage(): JSX.Element {
changedSortedSearch.name = nameInputText
changedSortedSearch.filter = queryInputText
setSortedSavedSearch(sortedSavedSearch)
await updateFilterMutation({
id,
name: nameInputText,
filter: queryInputText,
await updateSavedSearch.mutateAsync({
input: {
id,
name: nameInputText,
filter: queryInputText,
},
})
}
}
@ -239,14 +247,14 @@ export default function SavedSearchesPage(): JSX.Element {
}
}
async function onDeleteSavedSearch(id: string): Promise<void> {
const currentElement = sortedSavedSearch?.find((it) => it.id == id)
async function onDeleteSavedSearch(searchId: string): Promise<void> {
const currentElement = sortedSavedSearch?.find((it) => it.id == searchId)
if (currentElement) {
await deleteFilterMutation(id)
await deleteSavedSearch.mutateAsync({ searchId })
setSortedSavedSearch(
sortedSavedSearch
.filter((it) => it.id !== id)
.filter((it) => it.id !== searchId)
.map((it) => {
return {
...it,
@ -262,7 +270,7 @@ export default function SavedSearchesPage(): JSX.Element {
return
}
async function deleteSavedSearch(id: string): Promise<void> {
async function doDeleteSavedSearch(id: string): Promise<void> {
setConfirmRemoveSavedSearchId(id)
}
@ -306,10 +314,14 @@ export default function SavedSearchesPage(): JSX.Element {
})
?.sort((l, r) => l.position - r.position)
setSortedSavedSearch(newlyOrdered)
return updateFilterMutation({
...currentElement,
position: correctedIdx,
})
return (
await updateSavedSearch.mutateAsync({
input: {
...currentElement,
position: correctedIdx,
},
})
)?.id
}
}
@ -409,14 +421,14 @@ export default function SavedSearchesPage(): JSX.Element {
editingId={editingId}
setEditingId={setEditingId}
isCreateMode={isCreateMode}
deleteSavedSearch={deleteSavedSearch}
deleteSavedSearch={doDeleteSavedSearch}
nameInputText={nameInputText}
queryInputText={queryInputText}
setNameInputText={setNameInputText}
setQueryInputText={setQueryInputText}
setIsCreateMode={setIsCreateMode}
createSavedSearch={createSavedSearch}
updateSavedSearch={updateSavedSearch}
createSavedSearch={doCreateSavedSearch}
updateSavedSearch={doUpdateSavedSearch}
onEditPress={onEditPress}
resetState={resetSavedSearchState}
draggedElementId={draggedElementId}
@ -429,14 +441,14 @@ export default function SavedSearchesPage(): JSX.Element {
editingId={editingId}
setEditingId={setEditingId}
isCreateMode={isCreateMode}
deleteSavedSearch={deleteSavedSearch}
deleteSavedSearch={doDeleteSavedSearch}
nameInputText={nameInputText}
queryInputText={queryInputText}
setNameInputText={setNameInputText}
setQueryInputText={setQueryInputText}
setIsCreateMode={setIsCreateMode}
createSavedSearch={createSavedSearch}
updateSavedSearch={updateSavedSearch}
createSavedSearch={doCreateSavedSearch}
updateSavedSearch={doUpdateSavedSearch}
onEditPress={onEditPress}
resetState={resetSavedSearchState}
draggedElementId={draggedElementId}
@ -465,9 +477,9 @@ export default function SavedSearchesPage(): JSX.Element {
setQueryInputText: setQueryInputText,
setIsCreateMode: setIsCreateMode,
resetState: resetSavedSearchState,
updateSavedSearch,
deleteSavedSearch,
createSavedSearch,
updateSavedSearch: doUpdateSavedSearch,
deleteSavedSearch: doDeleteSavedSearch,
createSavedSearch: doCreateSavedSearch,
draggedElementId,
setDraggedElementId,
onEditPress,
@ -579,6 +591,7 @@ function GenericTableCard(
editingId === savedSearch?.id || (isCreateMode && !savedSearch)
const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58'
const DEFAULT_STYLE = { position: null }
const updateSavedSearchFunc = useUpdateSavedSearch()
const [style, setStyle] = useState<
Partial<{
position: string | null
@ -697,7 +710,9 @@ function GenericTableCard(
}, [draggedElementId, onMouseMove])
const setVisibility = async () => {
await updateFilterMutation({ ...savedSearch, visible: !isVisible })
await updateSavedSearchFunc.mutateAsync({
input: { ...savedSearch, visible: !isVisible },
})
setIsVisible(!isVisible)
}

View File

@ -8,7 +8,6 @@ import {
} from 'react'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
import { SettingsLayout } from '../../components/templates/SettingsLayout'
import { Toaster } from 'react-hot-toast'
import {
@ -36,6 +35,7 @@ import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { escapeQuotes } from '../../utils/helper'
import { Shortcut } from '../../components/templates/navMenu/NavigationMenu'
import { useGetLabels } from '../../lib/networking/labels/useLabels'
import { useGetSavedSearches } from '../../lib/networking/savedsearches/useSavedSearches'
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
const SHORTCUTS_KEY = 'library-shortcuts'
@ -233,7 +233,7 @@ type ListProps = {
const AvailableItems = (props: ListProps): JSX.Element => {
const { data: labels } = useGetLabels()
const { savedSearches } = useGetSavedSearchQuery()
const { data: savedSearches } = useGetSavedSearches()
const { subscriptions } = useGetSubscriptionsQuery()
const sortedLabels = useMemo(() => {