mutation update for collections

This commit is contained in:
Nate Kelley 2025-02-13 15:15:32 -07:00
parent 5d283d49d3
commit a9f7cf23cc
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 180 additions and 110 deletions

View File

@ -1,6 +1,12 @@
'use client';
import { type UseQueryOptions, useMutation, useQueryClient } from '@tanstack/react-query';
import {
MutationFunction,
QueryKey,
type UseQueryOptions,
useMutation,
useQueryClient
} from '@tanstack/react-query';
import type {
BusterSocketRequest,
BusterSocketResponse,
@ -16,6 +22,62 @@ import type {
InferBusterSocketResponseData
} from './types';
/**
* A custom hook that combines WebSocket communication with React Query's mutation capabilities.
* This hook allows you to emit socket events and handle their responses while integrating with React Query's state management.
*
* @template TRequestRoute - The type of socket request route
* @template TRoute - The type of socket response route
* @template TError - The type of error that might occur during the mutation
* @template TData - The type of data returned by the socket response
* @template TPayload - The type of payload sent in the socket request
* @template TQueryData - The type of data stored in the React Query cache
*
* @param socketRequest - Configuration object for the socket request
* @param socketResponse - Configuration object for handling the socket response
* @param options - React Query options for the mutation
* @param preCallback - Optional callback function executed before the socket request, allowing manipulation of cached data
* @param callback - Optional callback function executed after receiving the socket response, allowing transformation of the response data
*
* @returns A mutation object from React Query that can be used to trigger the socket request
*
* @example
* ```tsx
* // Example usage in a component
* function ChatComponent() {
* const mutation = useSocketQueryMutation(
* { route: 'chat:send' },
* { route: 'chat:message' },
* {
* queryKey: ['chat', 'messages']
* },
* // Pre-callback: Optimistically update the UI
* (currentMessages, newMessage) => {
* return [...currentMessages, { pending: true, ...newMessage }];
* },
* // Post-callback: Update with the actual response
* (socketResponse, currentMessages) => {
* return currentMessages.map(msg =>
* msg.pending ? socketResponse : msg
* );
* }
* );
*
* const sendMessage = (message: string) => {
* mutation.mutate({ content: message });
* };
*
* return (
* <button
* onClick={() => sendMessage('Hello!')}
* disabled={mutation.isPending}
* >
* Send Message
* </button>
* );
* }
* ```
*/
export function useSocketQueryMutation<
TRequestRoute extends BusterSocketRequestRoute,
TRoute extends BusterSocketResponseRoute,
@ -26,7 +88,7 @@ export function useSocketQueryMutation<
>(
socketRequest: BusterSocketRequestConfig<TRequestRoute>,
socketResponse: BusterSocketResponseConfig<TRoute>,
options?: UseQueryOptions<TQueryData, any, TQueryData, any>,
options?: UseQueryOptions<TQueryData, any, TQueryData, any> | null,
preCallback?: (
currentData: TQueryData | null,
variables: TPayload
@ -39,41 +101,47 @@ export function useSocketQueryMutation<
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
const mutationFn = useMemoizedFn(async (variables: TPayload): Promise<TData> => {
const queryKey = options?.queryKey;
const mutationFn: MutationFunction<TData, TPayload> = useMemoizedFn(
async (variables: TPayload): Promise<TData> => {
const queryKey: QueryKey = options?.queryKey;
if (queryKey && preCallback) {
const currentData = queryClient.getQueryData<TQueryData>(queryKey) ?? null;
const transformedData = await preCallback(currentData, variables);
queryClient.setQueryData(queryKey, transformedData);
}
try {
const result = await busterSocket.emitAndOnce({
emitEvent: {
route: socketRequest.route,
payload: variables
} as BusterSocketRequest,
responseEvent: {
route: socketResponse.route,
onError: socketResponse.onError,
callback: (d: unknown) => d
} as BusterSocketResponse
});
if (queryKey && callback) {
const socketData = result as InferBusterSocketResponseData<TRoute>;
const currentData = queryClient.getQueryData<TQueryData>(queryKey) ?? null;
const transformedData = callback(socketData, currentData);
queryClient.setQueryData(queryKey, transformedData);
return result as TData;
if (preCallback) {
const currentData = queryKey
? (queryClient.getQueryData<TQueryData>(queryKey) ?? null)
: null;
const transformedData = await preCallback(currentData, variables);
if (queryKey) queryClient.setQueryData(queryKey, transformedData);
}
return result as TData;
} catch (error) {
throw error;
try {
const result = await busterSocket.emitAndOnce({
emitEvent: {
route: socketRequest.route,
payload: variables
} as BusterSocketRequest,
responseEvent: {
route: socketResponse.route,
onError: socketResponse.onError,
callback: (d: unknown) => d
} as BusterSocketResponse
});
if (callback) {
const socketData = result as InferBusterSocketResponseData<TRoute>;
const currentData = queryKey
? (queryClient.getQueryData<TQueryData>(queryKey) ?? null)
: null;
const transformedData = callback(socketData, currentData);
if (queryKey) queryClient.setQueryData(queryKey, transformedData);
return result as TData;
}
return result as TData;
} catch (error) {
throw error;
}
}
});
);
return useMutation<TData, TError, TPayload>({
mutationFn

View File

@ -1,20 +1,31 @@
import { useMemoizedFn } from 'ahooks';
import { useBusterWebSocket } from '../../BusterWebSocket';
import { useSocketQueryEmitAndOnce, useSocketQueryMutation } from '@/api/buster_socket_query';
import { useSocketQueryMutation } from '@/api/buster_socket_query';
import { queryKeys } from '@/api/asset_interfaces';
const getChatsListOptions = queryKeys['/chats/list:getChatsList']();
export const useChatAssosciations = () => {
const busterSocket = useBusterWebSocket();
const x = useSocketQueryMutation('');
const { mutate: deleteChat } = useSocketQueryMutation(
{ route: '/chats/delete' },
{ route: '/chats/delete:deleteChat' },
getChatsListOptions,
(currentData, deleteDataIds) => {
//TODO: maybe use query client to remove all the chats from the query cache?
const allDeleteDataIds = deleteDataIds.map((d) => d.id);
return currentData?.filter((chat) => !allDeleteDataIds.includes(chat.id)) || [];
}
);
const onDeleteChat = useMemoizedFn(async (chatId: string) => {
//
// await busterSocket.emit({
// route: '/chats/delete',
// payload: { id: chatId }
// });
deleteChat([{ id: chatId }]);
});
const onDeleteChats = useMemoizedFn(async (chatIds: string[]) => {
deleteChat(chatIds.map((id) => ({ id })));
});
return {
onDeleteChat
onDeleteChat,
onDeleteChats
};
};

View File

@ -1,35 +1,37 @@
import type { BusterCollection } from '@/api/asset_interfaces';
import type { CollectionUpdateCollection } from '@/api/buster_socket/collections';
import { useSocketQueryMutation } from '@/hooks';
import { useSocketQueryMutation } from '@/api/buster_socket_query';
import { useMemoizedFn } from 'ahooks';
import { BusterCollection, BusterCollectionListItem, queryKeys } from '@/api/asset_interfaces';
import { useQueryClient } from '@tanstack/react-query';
export const useCollectionUpdate = () => {
const queryClient = useQueryClient();
const { mutateAsync: updateCollection, isPending: isUpdatingCollection } = useSocketQueryMutation(
{ route: '/collections/update' },
{ route: '/collections/update:collectionState' },
{
preSetQueryData: [
{
responseRoute: '/collections/get:collectionState',
callback: (data, _variables) => {
const variables = _variables as Partial<BusterCollection>;
const newObject: BusterCollection = { ...data!, ...variables };
return newObject;
}
},
{
responseRoute: '/collections/list:listCollections',
callback: (data, _variables) => {
const existingData = data || [];
const variables = _variables as Partial<BusterCollection>;
return existingData.map((collection) =>
collection.id === variables.id
? { ...collection, name: variables.name || collection.name }
: collection
);
}
}
]
null,
(_, variables) => {
const collectionId = variables.id!;
const collectionOptions = queryKeys['/collections/get:collectionState'](collectionId);
const queryKey = collectionOptions.queryKey;
const collection = queryClient.getQueryData(queryKey);
if (collection) {
const newCollection: BusterCollection = {
...collection!,
...(variables as Partial<BusterCollection>)
};
queryClient.setQueryData(queryKey, newCollection);
}
const collectionListOptions = queryKeys['/collections/list:getCollectionsList']();
const collectionList = queryClient.getQueryData(collectionListOptions.queryKey);
if (collectionList && variables.name) {
const newCollectionList: BusterCollectionListItem[] = collectionList.map((collection) =>
collection.id === collectionId ? { ...collection, name: variables.name! } : collection
);
queryClient.setQueryData(collectionListOptions.queryKey, newCollectionList);
}
}
);

View File

@ -1,38 +1,25 @@
import React, { PropsWithChildren, useLayoutEffect, useState } from 'react';
import { useMemoizedFn, useUnmount } from 'ahooks';
import React, { PropsWithChildren, useState } from 'react';
import { useMemoizedFn } from 'ahooks';
import {
useContextSelector,
createContext,
ContextSelector
} from '@fluentui/react-context-selector';
import { BusterDashboardResponse } from '@/api/asset_interfaces';
import { queryKeys } from '@/api/asset_interfaces';
import { useDashboardAssosciations } from './useDashboardAssosciations';
import { useDashboardCreate } from './useDashboardCreate';
import { useDashboardUpdateConfig } from './useDashboardUpdateConfig';
import { createQueryKey } from '@/hooks';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
import { useQueryClient } from '@tanstack/react-query';
export const useBusterDashboards = () => {
const [openAddContentModal, setOpenAddContentModal] = useState(false);
const queryClient = useQueryClient();
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);
const getDashboard = useQuery({
queryKey: ['/dashboards/get:getDashboardState', { id: '1' }],
queryFn: () => {
return { id: '1' };
},
enabled: false
});
const getDashboardMemoized = useMemoizedFn((dashboardId: string) => {
const { password } = getAssetPassword(dashboardId);
const queryKey = createQueryKey(
{ route: '/dashboards/get:getDashboardState' },
{ route: '/dashboards/get', payload: { id: dashboardId, password } }
);
return queryClient.getQueryData<BusterDashboardResponse>(queryKey);
const options = queryKeys['/dashboards/get:getDashboardState'](dashboardId);
const queryKey = options.queryKey;
const data = queryClient.getQueryData(queryKey);
return data;
});
const dashboardUpdateConfig = useDashboardUpdateConfig({ getDashboardMemoized });

View File

@ -1,9 +1,7 @@
import type { BusterDashboardResponse } from '@/api/asset_interfaces';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useMemoizedFn } from 'ahooks';
import { useBusterDashboardListContextSelector } from '../DashboardListProvider/DashboardListProvider';
import { useSocketQueryMutation } from '@/hooks';
export const useDashboardAssosciations = () => {
const busterSocket = useBusterWebSocket();

View File

@ -1,13 +1,15 @@
import type {
BusterDashboard,
BusterDashboardResponse,
VerificationStatus
import {
queryKeys,
type BusterDashboard,
type BusterDashboardResponse,
type VerificationStatus
} from '@/api/asset_interfaces';
import { DashboardUpdate } from '@/api/buster_socket/dashboards';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useSocketQueryMutation } from '@/hooks';
import { useSocketQueryMutation } from '@/api/buster_socket_query';
import { useMemoizedFn } from 'ahooks';
import { create } from 'mutative';
import { useQueryClient } from '@tanstack/react-query';
export const useDashboardUpdateConfig = ({
getDashboardMemoized
@ -15,24 +17,25 @@ export const useDashboardUpdateConfig = ({
getDashboardMemoized: (dashboardId: string) => BusterDashboardResponse | undefined;
}) => {
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
const { mutateAsync: updateDashboard, isPending: isUpdatingDashboard } = useSocketQueryMutation(
{ route: '/dashboards/update' },
{ route: '/dashboards/update:updateDashboard' },
{
preSetQueryData: [
{
responseRoute: '/dashboards/get:getDashboardState',
callback: (data, variables) => {
const newObject: BusterDashboardResponse = create(data!, (draft) => {
Object.assign(draft.dashboard, variables, {
config: { ...draft.dashboard.config, ...variables.config }
});
});
return newObject;
}
}
]
null,
(_, variables) => {
const options = queryKeys['/dashboards/get:getDashboardState'](variables.id);
const queryKey = options.queryKey;
const currentData = queryClient.getQueryData(queryKey);
if (currentData) {
const newObject: BusterDashboardResponse = create(currentData, (draft) => {
Object.assign(draft.dashboard, variables, {
config: { ...draft.dashboard.config, ...variables.config }
});
});
queryClient.setQueryData(queryKey, newObject);
}
return null;
}
);
@ -93,6 +96,7 @@ export const useDashboardUpdateConfig = ({
});
return {
isUpdatingDashboard,
onShareDashboard,
onUpdateDashboardConfig,
onUpdateDashboard,