ultisuite-client/lib/api/hooks/use-contact-mutations.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

274 lines
8.5 KiB
TypeScript

'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useDemoContacts } from '@/lib/demo/demo-contacts-context'
import { DEMO_CONTACTS_QUERY_ROOT } from '@/lib/demo/demo-contacts-bootstrap'
import { useDemoContactsStore } from '@/lib/demo/demo-contacts-store'
import { apiClient, OfflineError } from '../client'
import { enqueue } from '../offline-queue'
import { fullContactToApiContact, buildContactUpdatePayload } from '../adapters'
import {
appendContactToBookCache,
replaceContactInBookCaches,
} from '../contact-book-cache'
import { invalidateContactListCache } from '../contact-list-cache'
import { contactApiPath } from '@/lib/contacts/contact-api-path'
import { mergeTwoContacts, mergeManyContacts } from '@/lib/contacts/merge-contacts'
import type { FullContact } from '@/lib/contacts/types'
import type { ApiContact } from '../types'
export function useCreateContact() {
const queryClient = useQueryClient()
const demoContacts = useDemoContacts()
const isDemoContacts = demoContacts?.enabled ?? false
return useMutation({
mutationFn: async (vars: { bookId: string; contact: Partial<ApiContact> }) => {
if (isDemoContacts) {
return useDemoContactsStore
.getState()
.createContact(vars.bookId, vars.contact)
}
const created = await apiClient.post<ApiContact | undefined>(
`/contacts/books/${vars.bookId}`,
vars.contact,
)
if (created?.uid) return created
return vars.contact as ApiContact
},
onSuccess: (data, vars) => {
if (isDemoContacts) {
useDemoContactsStore.getState().bump()
void queryClient.invalidateQueries({ queryKey: DEMO_CONTACTS_QUERY_ROOT })
demoContacts?.notify('Contact créé')
return
}
const contact = data?.uid ? data : (vars.contact as ApiContact)
if (contact?.uid) {
appendContactToBookCache(queryClient, vars.bookId, {
...contact,
etag: contact.etag ?? data?.etag,
path: contact.path ?? data?.path,
})
}
invalidateContactListCache(vars.bookId)
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()
const demoContacts = useDemoContacts()
const isDemoContacts = demoContacts?.enabled ?? false
return useMutation({
mutationFn: async (vars: {
path: string
contact: Partial<ApiContact>
etag?: string
skipInvalidation?: boolean
}) => {
if (isDemoContacts) {
useDemoContactsStore.getState().updateContact(vars.path, vars.contact)
return { etag: `"demo-v2"` }
}
const ifMatch = vars.etag ?? vars.contact.etag
const headers: Record<string, string> = {}
if (ifMatch) {
headers['If-Match'] = ifMatch
}
const { etag: _bodyEtag, ...contactBody } = vars.contact
const apiPath = vars.path.replace(/^\/+/, '')
return apiClient.put<{ etag?: string }>(`/contacts/${apiPath}`, contactBody, headers)
},
onSuccess: (data, vars) => {
if (isDemoContacts) {
useDemoContactsStore.getState().bump()
void queryClient.invalidateQueries({ queryKey: DEMO_CONTACTS_QUERY_ROOT })
return
}
const apiPath = vars.path.replace(/^\/+/, '')
const nextEtag = data?.etag ?? vars.etag
replaceContactInBookCaches(queryClient, apiPath, {
...vars.contact,
etag: nextEtag,
})
invalidateContactListCache()
if (!vars.skipInvalidation) {
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()
const demoContacts = useDemoContacts()
const isDemoContacts = demoContacts?.enabled ?? false
return useMutation({
mutationFn: async (vars: { path: string; bookId?: string }) => {
if (isDemoContacts) {
useDemoContactsStore.getState().deleteContact(vars.path)
return
}
return 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: () => {
if (isDemoContacts) {
useDemoContactsStore.getState().bump()
void queryClient.invalidateQueries({ queryKey: DEMO_CONTACTS_QUERY_ROOT })
demoContacts?.notify('Contact supprimé')
return
}
invalidateContactListCache()
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) => {
invalidateContactListCache(vars.bookId)
queryClient.invalidateQueries({ queryKey: ['contacts', vars.bookId] })
},
})
}
export function useMergeContactPair() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (vars: {
bookId: string
contactA: FullContact
contactB: FullContact
}) => {
const { primary, secondary, merged } = mergeTwoContacts(vars.contactA, vars.contactB)
const primaryPath = contactApiPath(primary)
const secondaryPath = contactApiPath(secondary)
const headers: Record<string, string> = {}
if (primary.etag) {
headers['If-Match'] = primary.etag
}
await apiClient.put(
`/contacts/${primaryPath}`,
fullContactToApiContact(merged),
headers,
)
if (secondaryPath !== primaryPath) {
await apiClient.delete(`/contacts/${secondaryPath}`)
}
},
onSuccess: (_data, vars) => {
invalidateContactListCache(vars.bookId)
queryClient.invalidateQueries({ queryKey: ['contacts', vars.bookId] })
},
})
}
export function useMergeManyContacts() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (vars: {
bookId: string
contacts: FullContact[]
primaryId?: string
}) => {
const result = mergeManyContacts(vars.contacts, vars.primaryId)
if (!result) {
throw new Error('At least 2 contacts are required to merge')
}
const { primary, secondaries, merged } = result
if (!primary.etag) {
throw new Error('Cannot merge: unknown contact version. Reload the list.')
}
const headers: Record<string, string> = {
'If-Match': primary.etag,
}
await apiClient.put(
`/contacts/${contactApiPath(primary)}`,
fullContactToApiContact(merged),
headers,
)
for (const secondary of secondaries) {
const secondaryPath = contactApiPath(secondary)
if (secondaryPath !== contactApiPath(primary)) {
await apiClient.delete(`/contacts/${secondaryPath}`)
}
}
},
onSuccess: (_data, vars) => {
invalidateContactListCache(vars.bookId)
queryClient.invalidateQueries({ queryKey: ['contacts', vars.bookId] })
},
})
}