Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Move mail, compose, contacts, and accounts off mocks onto REST + WS. Add client, auth store, IDB-backed query cache, offline queue, and sync bar; hybrid Zustand for UI-only state. Settings still local until backend has preferences API.
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
'use client'
|
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { apiClient, OfflineError } from '../client'
|
|
import { enqueue } from '../offline-queue'
|
|
import type { ApiContact } from '../types'
|
|
|
|
export function useCreateContact() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (vars: { bookId: string; contact: Partial<ApiContact> }) =>
|
|
apiClient.post<ApiContact>(`/contacts/books/${vars.bookId}`, vars.contact),
|
|
onSuccess: (_data, vars) => {
|
|
queryClient.invalidateQueries({ queryKey: ['contacts', vars.bookId] })
|
|
},
|
|
onError: (err, vars) => {
|
|
if (err instanceof OfflineError) {
|
|
enqueue({
|
|
id: crypto.randomUUID(),
|
|
timestamp: Date.now(),
|
|
type: 'create_contact',
|
|
payload: { bookId: vars.bookId, ...vars.contact },
|
|
retries: 0,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useUpdateContact() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (vars: {
|
|
path: string
|
|
contact: Partial<ApiContact>
|
|
etag?: string
|
|
}) => apiClient.put<ApiContact>(`/contacts/${vars.path}`, {
|
|
...vars.contact,
|
|
etag: vars.etag,
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['contacts'] })
|
|
},
|
|
onError: (err, vars) => {
|
|
if (err instanceof OfflineError) {
|
|
enqueue({
|
|
id: crypto.randomUUID(),
|
|
timestamp: Date.now(),
|
|
type: 'update_contact',
|
|
payload: { path: vars.path, ...vars.contact },
|
|
retries: 0,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeleteContact() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (vars: { path: string; bookId?: string }) =>
|
|
apiClient.delete(`/contacts/${vars.path}`),
|
|
onMutate: async (vars) => {
|
|
await queryClient.cancelQueries({ queryKey: ['contacts'] })
|
|
const queries = queryClient.getQueriesData<ApiContact[]>({ queryKey: ['contacts'] })
|
|
const snapshots: [readonly unknown[], ApiContact[] | undefined][] = []
|
|
|
|
for (const [key, data] of queries) {
|
|
if (data) {
|
|
snapshots.push([key, data])
|
|
queryClient.setQueryData(
|
|
key,
|
|
data.filter((c) => c.path !== vars.path && c.uid !== vars.path)
|
|
)
|
|
}
|
|
}
|
|
return { snapshots }
|
|
},
|
|
onError: (err, vars, context) => {
|
|
if (context?.snapshots) {
|
|
for (const [key, data] of context.snapshots) {
|
|
queryClient.setQueryData(key, data)
|
|
}
|
|
}
|
|
if (err instanceof OfflineError) {
|
|
enqueue({
|
|
id: crypto.randomUUID(),
|
|
timestamp: Date.now(),
|
|
type: 'delete_contact',
|
|
payload: { path: vars.path, uid: vars.path },
|
|
retries: 0,
|
|
})
|
|
}
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['contacts'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useMergeDuplicates() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (vars: { bookId: string }) =>
|
|
apiClient.post(`/contacts/books/${vars.bookId}/merge-duplicates`),
|
|
onSuccess: (_data, vars) => {
|
|
queryClient.invalidateQueries({ queryKey: ['contacts', vars.bookId] })
|
|
},
|
|
})
|
|
}
|