diff --git a/jest.config.ts b/jest.config.ts index 64287715..3107303f 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -125,7 +125,7 @@ export default { setupFiles: ['/test/setupTests.ts'], // A list of paths to modules that run some code to configure or set up the testing framework before each test - setupFilesAfterEnv: ['/test/setupTestsAfterEnv.ts'], + // setupFilesAfterEnv: ['/test/setupTestsAfterEnv.ts'], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/package.json b/package.json index f5b5a993..3f35d930 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "axios": "0.27.2", "dexie": "3.2.4", "http-status-codes": "2.3.0", - "immutable": "4.3.4", "miragejs": "0.1.48", "msw": "1.3.2", "qs": "6.11.2", diff --git a/src/components/withContext.tsx b/src/components/withContext.tsx index f2a7ec6a..1ded616d 100644 --- a/src/components/withContext.tsx +++ b/src/components/withContext.tsx @@ -1,11 +1,11 @@ import React, { ReactElement, createContext, useContext } from 'react'; -import { Context, PermissionLevel, convertJs } from '@graasp/sdk'; +import { Context, PermissionLevel, } from '@graasp/sdk'; import { UseQueryResult } from '@tanstack/react-query'; -import { LocalContext, LocalContextRecord } from '../types'; import { AutoResizer } from './AutoResizer'; +import { LocalContext } from '../types'; export const defaultContextValue: LocalContext = { apiHost: '', @@ -20,14 +20,14 @@ export const defaultContextValue: LocalContext = { permission: PermissionLevel.Read, }; -const LocalContextContext = createContext(convertJs(defaultContextValue)); +const LocalContextContext = createContext((defaultContextValue)); interface WithLocalContextProps { children: ReactElement; useGetLocalContext: ( itemId: string, defaultValue: LocalContext, - ) => UseQueryResult; + ) => UseQueryResult; LoadingComponent?: React.ReactElement; defaultValue: LocalContext; onError?: (error: unknown) => void; @@ -90,7 +90,7 @@ interface Props { useGetLocalContext: ( itemId: string, defaultValue: LocalContext, - ) => UseQueryResult; + ) => UseQueryResult; LoadingComponent?: React.ReactElement; defaultValue: LocalContext; onError?: (error: unknown) => void; @@ -146,6 +146,6 @@ const withContext =

( return WithContextComponent; }; -const useLocalContext = (): LocalContextRecord => useContext(LocalContextContext); +const useLocalContext = (): LocalContext => useContext(LocalContextContext); export { useLocalContext, WithLocalContext, withContext }; diff --git a/src/config/keys.ts b/src/config/keys.ts index f6dac4cd..30a4516f 100644 --- a/src/config/keys.ts +++ b/src/config/keys.ts @@ -21,19 +21,6 @@ export const buildPostMessageKeys = (itemId: UUID) => POST_AUTO_RESIZE: `POST_AUTO_RESIZE_${itemId}`, }) as const; -export const MUTATION_KEYS = { - POST_APP_DATA: ['POST_APP_DATA'], - PATCH_APP_DATA: ['PATCH_APP_DATA'], - DELETE_APP_DATA: ['DELETE_APP_DATA'], - PATCH_SETTINGS: ['PATCH_SETTINGS'], - POST_APP_ACTION: ['POST_APP_ACTION'], - POST_APP_SETTING: ['POST_APP_SETTING'], - PATCH_APP_SETTING: ['PATCH_APP_SETTING'], - DELETE_APP_SETTING: ['DELETE_APP_SETTING'], - FILE_UPLOAD: ['FILE_UPLOAD'], - APP_SETTING_FILE_UPLOAD: ['APP_SETTING_FILE_UPLOAD'], -}; - export const QUERY_KEYS = { buildAppDataKey, buildAppContextKey, diff --git a/src/config/utils.ts b/src/config/utils.ts index d6256d9f..40552927 100644 --- a/src/config/utils.ts +++ b/src/config/utils.ts @@ -1,9 +1,7 @@ import { QueryClient } from '@tanstack/react-query'; -// eslint-disable-next-line import/no-extraneous-dependencies import { StatusCodes } from 'http-status-codes'; -import { Record } from 'immutable'; -import { LocalContext, LocalContextRecord, QueryClientConfig } from '../types'; +import { LocalContext, QueryClientConfig } from '../types'; import { MissingAppKeyError, MissingAppOriginError, @@ -23,8 +21,8 @@ export class MissingApiHostError extends Error { } } -export const getApiHost = (queryClient: QueryClient): string => { - const context = queryClient.getQueryData(LOCAL_CONTEXT_KEY); +export const getApiHost = (queryClient: QueryClient) => { + const context = queryClient.getQueryData(LOCAL_CONTEXT_KEY); const apiHost = context?.apiHost; if (!apiHost) { throw new MissingApiHostError(); @@ -33,8 +31,8 @@ export const getApiHost = (queryClient: QueryClient): string => { }; export const getPermissionLevel = (queryClient: QueryClient) => { - const context = queryClient.getQueryData>(LOCAL_CONTEXT_KEY); - const permission = context?.get('permission'); + const context = queryClient.getQueryData(LOCAL_CONTEXT_KEY); + const permission = context?.permission; if (!permission) { throw new MissingPermissionError(); } @@ -45,7 +43,7 @@ export const getData = ( queryClient: QueryClient, options: { shouldMemberExist?: boolean } = {}, ): { itemId: string; memberId?: string; token: string } => { - const data = queryClient.getQueryData>(LOCAL_CONTEXT_KEY); + const data = queryClient.getQueryData(LOCAL_CONTEXT_KEY); if (!data) { throw new Error('`LocalContext` was undefined'); } @@ -53,8 +51,8 @@ export const getData = ( if (!token) { throw new MissingNecessaryDataError({ token }); } - const itemId = data.get('itemId'); - const memberId = data.get('memberId'); + const { itemId } = data; + const { memberId } = data; if (options.shouldMemberExist ?? true) { if (!memberId) { diff --git a/src/hooks/app.test.ts b/src/hooks/app.test.ts index 68dc1de9..b9df5a03 100644 --- a/src/hooks/app.test.ts +++ b/src/hooks/app.test.ts @@ -1,7 +1,4 @@ -import { convertJs } from '@graasp/sdk'; - import { StatusCodes } from 'http-status-codes'; -import { Record } from 'immutable'; import nock from 'nock'; import { v4 } from 'uuid'; @@ -23,7 +20,7 @@ const itemId = v4(); describe('App Hooks', () => { beforeEach(() => { queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); afterEach(() => { @@ -42,10 +39,10 @@ describe('App Hooks', () => { const endpoints = [{ route, response }]; const { data } = await mockHook({ endpoints, hook, wrapper }); - expect(data?.toJS()).toEqual(response); + expect(data).toEqual(response); // verify cache keys - expect((queryClient.getQueryData(key) as Record).toJS()).toEqual(response); + expect(queryClient.getQueryData(key)).toEqual(response); }); it('Cannot fetch context if local context does not exist', async () => { const response = FIXTURE_CONTEXT; diff --git a/src/hooks/app.ts b/src/hooks/app.ts index 93fafd98..8db8f950 100644 --- a/src/hooks/app.ts +++ b/src/hooks/app.ts @@ -1,11 +1,9 @@ -import { convertJs } from '@graasp/sdk'; - import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; import { buildAppContextKey } from '../config/keys'; import { getApiHost, getDataOrThrow } from '../config/utils'; -import { AppContextRecord, QueryClientConfig } from '../types'; +import { AppContext, QueryClientConfig } from '../types'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default (queryConfig: QueryClientConfig) => { @@ -25,12 +23,12 @@ export default (queryConfig: QueryClientConfig) => { return useQuery({ queryKey: buildAppContextKey(itemId), - queryFn: (): Promise => + queryFn: (): Promise => Api.getContext({ itemId, token, apiHost, - }).then((data) => convertJs(data)), + }), ...defaultOptions, }); }, diff --git a/src/hooks/appAction.ts b/src/hooks/appAction.ts index 4c84d590..ac26ddf7 100644 --- a/src/hooks/appAction.ts +++ b/src/hooks/appAction.ts @@ -1,8 +1,6 @@ -import { PermissionLevel, convertJs } from '@graasp/sdk'; -import { AppActionRecord } from '@graasp/sdk/frontend'; +import { PermissionLevel } from '@graasp/sdk'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { List } from 'immutable'; import * as Api from '../api'; import { buildAppActionsKey } from '../config/keys'; @@ -35,10 +33,10 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien return useQuery({ queryKey: buildAppActionsKey(itemId), - queryFn: (): Promise> => { + queryFn: () => { const { token } = getDataOrThrow(queryClient); - return Api.getAppActions({ itemId, token, apiHost }).then((data) => convertJs(data)); + return Api.getAppActions({ itemId, token, apiHost }).then((data) => data); }, ...defaultOptions, enabled, diff --git a/src/hooks/appData.test.ts b/src/hooks/appData.test.ts index 7bd83f28..d4020fac 100644 --- a/src/hooks/appData.test.ts +++ b/src/hooks/appData.test.ts @@ -1,5 +1,3 @@ -import { convertJs } from '@graasp/sdk'; - import { StatusCodes } from 'http-status-codes'; import nock from 'nock'; import { v4 } from 'uuid'; @@ -27,7 +25,7 @@ const itemId = v4(); describe('App Data Hooks', () => { beforeEach(() => { queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); afterEach(() => { @@ -45,16 +43,16 @@ describe('App Data Hooks', () => { const endpoints = [{ route, response }]; const { data } = await mockHook({ endpoints, hook, wrapper }); - expect(data?.toJS()).toEqual(response); + expect(data).toEqual(response); // verify cache keys - expect(queryClient.getQueryData(key)).toEqualImmutable(convertJs(response)); + expect(queryClient.getQueryData(key)).toEqual(response); }); it('Cannot fetch app data if context does not exist', async () => { - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), apiHost: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + apiHost: null, + }); const response = FIXTURE_APP_DATA; const endpoints = [{ route, response }]; diff --git a/src/hooks/appData.ts b/src/hooks/appData.ts index 2b2497dd..1d6323f9 100644 --- a/src/hooks/appData.ts +++ b/src/hooks/appData.ts @@ -1,8 +1,4 @@ -import { convertJs } from '@graasp/sdk'; -import { AppDataRecord } from '@graasp/sdk/frontend'; - import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { List } from 'immutable'; import * as Api from '../api'; import { MissingFileIdError } from '../config/errors'; @@ -40,9 +36,9 @@ export default ( return useQuery({ queryKey: buildAppDataKey(itemId), - queryFn: (): Promise> => { + queryFn: () => { const { token } = getDataOrThrow(queryClient); - return Api.getAppData({ itemId, token, apiHost }).then((data) => convertJs(data)); + return Api.getAppData({ itemId, token, apiHost }); }, ...defaultOptions, refetchInterval, diff --git a/src/hooks/appSetting.test.ts b/src/hooks/appSetting.test.ts index 6a3913ee..a53fa7f1 100644 --- a/src/hooks/appSetting.test.ts +++ b/src/hooks/appSetting.test.ts @@ -1,5 +1,3 @@ -import { convertJs } from '@graasp/sdk'; - import { StatusCodes } from 'http-status-codes'; import nock from 'nock'; import { v4 } from 'uuid'; @@ -27,7 +25,7 @@ const itemId = v4(); describe('App Settings Hooks', () => { beforeEach(() => { queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); afterEach(() => { @@ -45,10 +43,10 @@ describe('App Settings Hooks', () => { const endpoints = [{ route, response }]; const { data } = await mockHook({ endpoints, hook, wrapper }); - expect(data?.toJS()).toEqual(response); + expect(data).toEqual(response); // verify cache keys - expect(queryClient.getQueryData(key)).toEqualImmutable(convertJs(response)); + expect(queryClient.getQueryData(key)).toEqual(response); }); it('Cannot fetch app settings if context does not exist', async () => { const response = FIXTURE_APP_SETTINGS; diff --git a/src/hooks/appSetting.ts b/src/hooks/appSetting.ts index 95c74143..f41f1bde 100644 --- a/src/hooks/appSetting.ts +++ b/src/hooks/appSetting.ts @@ -1,8 +1,6 @@ -import { convertJs } from '@graasp/sdk'; import { AppSettingRecord } from '@graasp/sdk/frontend'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { List } from 'immutable'; import * as Api from '../api'; import { MissingFileIdError } from '../config/errors'; @@ -34,7 +32,7 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien return useQuery({ queryKey: buildAppSettingsKey(itemId), - queryFn: (): Promise> => { + queryFn: () => { const { token: localToken, itemId: localItemId } = getDataOrThrow(queryClient, { shouldMemberExist: false, }); @@ -43,7 +41,7 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien itemId: localItemId, token: localToken, apiHost, - }).then((data) => convertJs(data)); + }); }, ...defaultOptions, enabled: Boolean(itemId) && Boolean(token), @@ -75,7 +73,7 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien id: appSettingId, apiHost, token: localToken, - }).then((data) => data); + }); }, ...defaultOptions, enabled: Boolean(payload?.appSettingId) && Boolean(token) && enabled, diff --git a/src/hooks/postMessage.test.ts b/src/hooks/postMessage.test.ts index e05f77e2..2a7b6d68 100644 --- a/src/hooks/postMessage.test.ts +++ b/src/hooks/postMessage.test.ts @@ -13,6 +13,7 @@ import { MissingMessageChannelPortError, } from '../config/errors'; import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildPostMessageKeys } from '../config/keys'; +import { LocalContext } from '../types'; const mockItemId = 'mock-item-id'; const POST_MESSAGE_KEYS = buildPostMessageKeys(mockItemId); @@ -43,8 +44,7 @@ describe.skip('PostMessage Hooks', () => { mockWindowForPostMessage(event); const { data } = await mockHook({ hook, wrapper }); - - const context = data?.toJS(); + const context = data as LocalContext; expect(context).toEqual({ apiHost: undefined, // @see LocalContextRecord memberId: undefined, // @see LocalContextRecord @@ -85,7 +85,7 @@ describe.skip('PostMessage Hooks', () => { const { data } = await mockHook({ hook, wrapper }); - expect(data?.toJS()).toEqual({ + expect(data).toEqual({ ...serverResponse, standalone: false, }); diff --git a/src/hooks/postMessage.ts b/src/hooks/postMessage.ts index 2d9fe9f0..88a7a162 100644 --- a/src/hooks/postMessage.ts +++ b/src/hooks/postMessage.ts @@ -4,9 +4,6 @@ */ import { useEffect } from 'react'; -import { convertJs } from '@graasp/sdk'; -import { ImmutableCast } from '@graasp/sdk/frontend'; - import { UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; @@ -15,7 +12,7 @@ import { MissingMessageChannelPortError } from '../config/errors'; import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildPostMessageKeys } from '../config/keys'; import { buildAppKeyAndOriginPayload } from '../config/utils'; import { getAuthTokenRoutine, getLocalContextRoutine } from '../routines'; -import { LocalContext, LocalContextRecord, QueryClientConfig, WindowPostMessage } from '../types'; +import { LocalContext, QueryClientConfig, WindowPostMessage } from '../types'; // build context from given data and default values export const buildContext = (payload: LocalContext): LocalContext => { @@ -108,7 +105,7 @@ const configurePostMessageHooks = (queryConfig: QueryClientConfig) => { const useGetLocalContext = ( itemId: string, defaultValue: LocalContext, - ): UseQueryResult => { + ): UseQueryResult => { let getLocalContextFunction: ((event: MessageEvent) => void) | null = null; const queryClient = useQueryClient(); return useQuery({ @@ -121,12 +118,12 @@ const configurePostMessageHooks = (queryConfig: QueryClientConfig) => { const newContext = await Api.getMockContext({ token: authToken, }); - return convertJs(buildContext(newContext)); + return buildContext(newContext); } console.debug( '[app-get-local-context] token was not found in data cache, using default value', ); - return convertJs(buildContext(defaultValue)); + return buildContext(defaultValue); } const POST_MESSAGE_KEYS = buildPostMessageKeys(itemId); const postMessagePayload = buildAppKeyAndOriginPayload(queryConfig); @@ -134,7 +131,7 @@ const configurePostMessageHooks = (queryConfig: QueryClientConfig) => { const formatResolvedValue = (result: { event: MessageEvent; payload: LocalContext; - }): LocalContextRecord => { + }): LocalContext => { const { event, payload } = result; // get init message getting the Message Channel port const context = buildContext(payload); @@ -142,10 +139,10 @@ const configurePostMessageHooks = (queryConfig: QueryClientConfig) => { // will use port for further communication // set as a global variable [port2] = event.ports; - return convertJs(context); + return context; }; - return new Promise>((resolve, reject) => { + return new Promise((resolve, reject) => { getLocalContextFunction = receiveContextMessage( POST_MESSAGE_KEYS.GET_CONTEXT_SUCCESS, POST_MESSAGE_KEYS.GET_CONTEXT_FAILURE, diff --git a/src/mutations/appAction.ts b/src/mutations/appAction.ts index 3f2918cd..23d2e07c 100644 --- a/src/mutations/appAction.ts +++ b/src/mutations/appAction.ts @@ -1,53 +1,49 @@ -import { AppAction, convertJs } from '@graasp/sdk'; -import { AppActionRecord } from '@graasp/sdk/frontend'; +import { AppAction } from '@graasp/sdk'; -import { QueryClient, useMutation } from '@tanstack/react-query'; -import { List } from 'immutable'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { MUTATION_KEYS, buildAppActionsKey } from '../config/keys'; +import { buildAppActionsKey } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { postAppActionRoutine } from '../routines'; import { QueryClientConfig } from '../types'; -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => { +export default (queryConfig: QueryClientConfig) => { const { enableWebsocket } = queryConfig; - queryClient.setMutationDefaults(MUTATION_KEYS.POST_APP_ACTION, { - mutationFn: (payload: Partial) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.postAppAction({ ...data, body: payload, apiHost }); - }, - onSuccess: (newAppAction: AppAction) => { - const { itemId } = getData(queryClient); - const key = buildAppActionsKey(itemId); - const prevData = queryClient.getQueryData>(key); - const newData: AppActionRecord = convertJs(newAppAction); - // check that the websocket event has not already been received and therefore the data were added - if (!prevData) { - queryClient.setQueryData(key, List.of(newData)); - } else if (!prevData.some((a) => a.id === newData.id)) { - queryClient.setQueryData(key, prevData?.push(newData)); - } - }, - onError: (error) => { - queryConfig?.notifier?.({ - type: postAppActionRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: () => { - if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppActionsKey(itemId)); - } - }, - }); - - return { - usePostAppAction: () => - useMutation>(MUTATION_KEYS.POST_APP_ACTION), + const usePostAppAction = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: Partial) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.postAppAction({ ...data, body: payload, apiHost }); + }, + { + onSuccess: (newAppAction: AppAction) => { + const { itemId } = getData(queryClient); + const key = buildAppActionsKey(itemId); + const prevData = queryClient.getQueryData(key); + const newData: AppAction = newAppAction; + // check that the websocket event has not already been received and therefore the data were added + if (!prevData) { + queryClient.setQueryData(key, newData); + } else if (!prevData.some((a) => a.id === newData.id)) { + queryClient.setQueryData(key, [...(prevData ?? []), newData]); + } + }, + onError: (error: Error) => { + queryConfig?.notifier?.({ type: postAppActionRoutine.FAILURE, payload: { error } }); + }, + onSettled: () => { + if (!enableWebsocket) { + const { itemId } = getData(queryClient); + queryClient.invalidateQueries(buildAppActionsKey(itemId)); + } + }, + }, + ); }; + + return { usePostAppAction }; }; diff --git a/src/mutations/appData.test.ts b/src/mutations/appData.test.ts index 5d91bd00..68f3acfb 100644 --- a/src/mutations/appData.test.ts +++ b/src/mutations/appData.test.ts @@ -1,9 +1,7 @@ -import { convertJs } from '@graasp/sdk'; -import { AppDataRecord } from '@graasp/sdk/frontend'; +import { AppData } from '@graasp/sdk'; import { act } from '@testing-library/react'; import { StatusCodes } from 'http-status-codes'; -import { List } from 'immutable'; import nock from 'nock'; import { v4 } from 'uuid'; @@ -21,7 +19,7 @@ import { buildPostAppDataRoute, } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, MUTATION_KEYS, buildAppDataKey } from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildAppDataKey } from '../config/keys'; import { patchAppDataRoutine, postAppDataRoutine } from '../routines'; const mockedNotifier = jest.fn(); @@ -35,11 +33,11 @@ describe('Apps Mutations', () => { nock.cleanAll(); }); - describe(MUTATION_KEYS.POST_APP_DATA[0], () => { + describe('usePostAppData', () => { const itemId = v4(); const key = buildAppDataKey(itemId); const toAdd = buildAppData(); - const initData = convertJs(FIXTURE_APP_DATA); + const initData = FIXTURE_APP_DATA; const route = `/${buildPostAppDataRoute({ itemId })}`; const mutation = mutations.usePostAppData; @@ -47,7 +45,7 @@ describe('Apps Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Create app data', async () => { @@ -75,9 +73,7 @@ describe('Apps Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable( - initData.push(convertJs(toAdd)), - ); + expect(queryClient.getQueryData(key)).toEqual([...initData, toAdd]); }); }); @@ -85,7 +81,7 @@ describe('Apps Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); @@ -117,16 +113,13 @@ describe('Apps Mutations', () => { }), ); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -153,17 +146,17 @@ describe('Apps Mutations', () => { type: postAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -181,7 +174,7 @@ describe('Apps Mutations', () => { }); await act(async () => { - await mockedMutation.mutate({ id: toAdd.id }); + mockedMutation.mutate(toAdd); await waitForMutation(); }); @@ -190,13 +183,13 @@ describe('Apps Mutations', () => { type: postAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); const endpoints = [ @@ -214,7 +207,7 @@ describe('Apps Mutations', () => { }); await act(async () => { - await mockedMutation.mutate({ id: toAdd.id }); + await mockedMutation.mutate(toAdd); await waitForMutation(); }); @@ -223,19 +216,19 @@ describe('Apps Mutations', () => { type: postAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); }); - describe(MUTATION_KEYS.PATCH_APP_DATA[0], () => { - const initData = convertJs(FIXTURE_APP_DATA); + describe('usePatchAppData', () => { + const initData = FIXTURE_APP_DATA; const itemId = v4(); - const appDataId = initData.first()?.id ?? v4(); + const appDataId = initData[0]?.id ?? v4(); const key = buildAppDataKey(itemId); const toPatch = buildAppData({ id: appDataId, data: { new: 'data' } }); - const updatedData = convertJs([toPatch, ...initData.delete(0).toJS()]); + const updatedData = [toPatch, ...initData.slice(1)]; const route = `/${buildPatchAppDataRoute({ id: toPatch.id, itemId })}`; const mutation = mutations.usePatchAppData; @@ -243,7 +236,7 @@ describe('Apps Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Patch app data', async () => { @@ -272,9 +265,9 @@ describe('Apps Mutations', () => { expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); // check data and length - const result = queryClient.getQueryData>(key); - expect(result?.first()?.data?.toJS()).toMatchObject(toPatch.data); - expect(result?.size).toBe(updatedData.size); + const result = queryClient.getQueryData(key); + expect(result?.[0]?.data).toMatchObject(toPatch.data); + expect(result?.length).toBe(updatedData.length); }); }); @@ -282,7 +275,7 @@ describe('Apps Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); @@ -314,16 +307,13 @@ describe('Apps Mutations', () => { }), ); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -350,17 +340,17 @@ describe('Apps Mutations', () => { type: patchAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -387,13 +377,13 @@ describe('Apps Mutations', () => { type: patchAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); const endpoints = [ @@ -420,13 +410,13 @@ describe('Apps Mutations', () => { type: patchAppDataRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); }); - describe(MUTATION_KEYS.DELETE_APP_DATA[0], () => { + describe('useDeleteAppData', () => { const itemId = v4(); const key = buildAppDataKey(itemId); const toDelete = FIXTURE_APP_DATA[0]; @@ -438,11 +428,11 @@ describe('Apps Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Delete app data', async () => { - const initData = convertJs([toDelete, FIXTURE_APP_DATA[1]]); + const initData = [toDelete, FIXTURE_APP_DATA[1]]; queryClient.setQueryData(key, initData); const endpoints = [ @@ -465,9 +455,7 @@ describe('Apps Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable( - convertJs([FIXTURE_APP_DATA[1]]), - ); + expect(queryClient.getQueryData(key)).toEqual([FIXTURE_APP_DATA[1]]); }); }); @@ -475,9 +463,9 @@ describe('Apps Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); - queryClient.setQueryData(key, convertJs([toDelete])); + queryClient.setQueryData(key, [toDelete]); const response = UNAUTHORIZED_RESPONSE; @@ -502,20 +490,15 @@ describe('Apps Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable( - convertJs([toDelete]), - ); + expect(queryClient.getQueryData(key)).toEqual([toDelete]); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); - const initData = convertJs([toDelete]); + const initData = [toDelete]; queryClient.setQueryData(key, initData); const endpoints = [ @@ -537,19 +520,19 @@ describe('Apps Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); - const initData = convertJs([toDelete]); + const initData = [toDelete]; queryClient.setQueryData(key, initData); const endpoints = [ @@ -571,15 +554,15 @@ describe('Apps Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); - const initData = convertJs([toDelete]); + const initData = [toDelete]; queryClient.setQueryData(key, initData); const endpoints = [ @@ -601,7 +584,7 @@ describe('Apps Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); diff --git a/src/mutations/appData.ts b/src/mutations/appData.ts index 36b49b2e..f2bb7a53 100644 --- a/src/mutations/appData.ts +++ b/src/mutations/appData.ts @@ -1,11 +1,9 @@ -import { AppData, UUID, convertJs } from '@graasp/sdk'; -import { AppDataRecord } from '@graasp/sdk/frontend'; +import { AppData, Member, UUID } from '@graasp/sdk'; -import { QueryClient, useMutation } from '@tanstack/react-query'; -import { List } from 'immutable'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { MUTATION_KEYS, buildAppDataKey } from '../config/keys'; +import { buildAppDataKey } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { deleteAppDataRoutine, @@ -15,185 +13,168 @@ import { } from '../routines'; import { QueryClientConfig } from '../types'; -export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => { +export default (queryConfig: QueryClientConfig) => { const { notifier, enableWebsocket } = queryConfig; - queryClient.setMutationDefaults(MUTATION_KEYS.POST_APP_DATA, { - mutationFn: (payload: Partial) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.postAppData({ ...data, body: payload, apiHost }); - }, - onSuccess: (newAppData: AppData) => { - const { itemId } = getData(queryClient); - const key = buildAppDataKey(itemId); - const prevData = queryClient.getQueryData>(key); - const newData: AppDataRecord = convertJs(newAppData); - if (!prevData) { - queryClient.setQueryData(key, List.of(newData)); - } else if (!prevData.some((a) => a.id === newData.id)) { - queryClient.setQueryData(key, prevData?.push(newData)); - } - queryConfig?.notifier?.({ type: postAppDataRoutine.SUCCESS, payload: newData }); - }, - onError: (error) => { - queryConfig?.notifier?.({ type: postAppDataRoutine.FAILURE, payload: { error } }); - }, - onSettled: () => { - if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppDataKey(itemId)); - } - }, - }); - const usePostAppData = () => - useMutation>(MUTATION_KEYS.POST_APP_DATA); + const usePostAppData = () => { + const queryClient = useQueryClient(); + return useMutation( + ( + payload: Pick & { memberId?: Member['id'] } & Partial< + Pick + >, + ) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.postAppData({ ...data, body: payload, apiHost }); + }, + { + onSuccess: (newAppData: AppData) => { + const { itemId } = getData(queryClient); + const key = buildAppDataKey(itemId); + const prevData = queryClient.getQueryData(key); + const newData: AppData = newAppData; + if (!prevData) { + queryClient.setQueryData(key, newData); + } else if (!prevData.some((a) => a.id === newData.id)) { + const newArray = [...(prevData ?? []), newData]; + queryClient.setQueryData(key, newArray); + } + queryConfig?.notifier?.({ type: postAppDataRoutine.SUCCESS, payload: newData }); + }, + onError: (error: Error) => { + queryConfig?.notifier?.({ type: postAppDataRoutine.FAILURE, payload: { error } }); + }, + onSettled: () => { + if (!enableWebsocket) { + const { itemId } = getData(queryClient); + queryClient.invalidateQueries(buildAppDataKey(itemId)); + } + }, + }, + ); + }; - queryClient.setMutationDefaults(MUTATION_KEYS.PATCH_APP_DATA, { - mutationFn: (payload: Partial & { id: UUID }) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.patchAppData({ ...data, ...payload, apiHost }); - }, - onMutate: async (payload) => { - let context = null; - const { itemId } = getData(queryClient); - const prevData = queryClient.getQueryData>(buildAppDataKey(itemId)); - if (itemId && prevData) { - const newData = prevData.map((appData) => - appData.id === payload.id ? appData.merge(convertJs(payload)) : appData, - ); - queryClient.setQueryData(buildAppDataKey(itemId), newData); - context = prevData; - } - return context; - }, - onSuccess: (newAppData) => { - queryConfig?.notifier?.({ type: patchAppDataRoutine.SUCCESS, payload: newAppData }); - }, - onError: (error, _payload, prevData) => { - queryConfig?.notifier?.({ type: patchAppDataRoutine.FAILURE, payload: { error } }); + const usePatchAppData = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: Partial> & { id: UUID }) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.patchAppData({ ...data, ...payload, apiHost }); + }, + { + onMutate: async (payload) => { + let context; + const { itemId } = getData(queryClient); + const prevData = queryClient.getQueryData(buildAppDataKey(itemId)); + if (itemId && prevData) { + const newData = prevData.map((appData) => + appData.id === payload.id ? { ...appData, ...payload } : appData, + ); + queryClient.setQueryData(buildAppDataKey(itemId), newData); + context = prevData; + } + return context; + }, + onSuccess: (newAppData) => { + queryConfig?.notifier?.({ type: patchAppDataRoutine.SUCCESS, payload: newAppData }); + }, + onError: (error: Error, _payload, prevData) => { + queryConfig?.notifier?.({ type: patchAppDataRoutine.FAILURE, payload: { error } }); - if (prevData) { - const { itemId } = getData(queryClient); - const data = queryClient.getQueryData>(buildAppDataKey(itemId)); - if (itemId && data) { - queryClient.setQueryData(buildAppDataKey(itemId), prevData); - } - } - }, - onSettled: () => { - if (!enableWebsocket) { - const data = getData(queryClient); - queryClient.invalidateQueries(buildAppDataKey(data?.itemId)); - } - }, - }); + if (prevData) { + const { itemId } = getData(queryClient); + const data = queryClient.getQueryData(buildAppDataKey(itemId)); + if (itemId && data) { + queryClient.setQueryData(buildAppDataKey(itemId), prevData); + } + } + }, + onSettled: () => { + if (!enableWebsocket) { + const data = getData(queryClient); + queryClient.invalidateQueries(buildAppDataKey(data?.itemId)); + } + }, + }, + ); + }; - /** - * @description - * By using the `merge` method in the `onMutate` callback, the payload should be a clean object, free of attributes from the `Record` type - * from Immutable.js. Therefore, when using this mutation, one must be careful not to use object spreading - * of the `data` attribute found in the `AppDataRecord`. I believe this may break some apps. - * - * @example Working example - * ``` - * const { id, data } = appData; // type: AppDataRecord - * // data: { text: string, count: number } - * patchAppData({ - * id, - * data: { - * count: data.count, - * text: newContent, - * }, - * }); - * ``` - * - * @example Failing example - * ``` - * const { id, data } = appData; // type: AppDataRecord - * // data: { text: string, count: number } - * patchAppData({ - * id, - * data: { - * ...data, - * text: newContent, - * }, - * }); - * ``` - * - */ - const usePatchAppData = () => - useMutation>(MUTATION_KEYS.PATCH_APP_DATA); - - queryClient.setMutationDefaults(MUTATION_KEYS.DELETE_APP_DATA, { - mutationFn: (payload: { id: string }) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.deleteAppData({ ...data, id: payload.id, apiHost }); - }, - onMutate: async (payload) => { - const { itemId } = getDataOrThrow(queryClient); - const prevData = queryClient.getQueryData>(buildAppDataKey(itemId)); - if (prevData && itemId) { - queryClient.setQueryData( - buildAppDataKey(itemId), - prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), - ); - } - return prevData; - }, - onSuccess: (prevData) => { - queryConfig?.notifier?.({ type: deleteAppDataRoutine.SUCCESS, payload: prevData }); - }, - onError: (error, _payload, prevData) => { - queryConfig?.notifier?.({ type: deleteAppDataRoutine.FAILURE, payload: { error } }); - if (prevData) { - const { itemId } = getData(queryClient); - const data = queryClient.getQueryData>(buildAppDataKey(itemId)); - if (itemId && data) { - queryClient.setQueryData(buildAppDataKey(itemId), prevData); - } - } - }, - onSettled: () => { - const { itemId } = getData(queryClient); - if (itemId && !enableWebsocket) { - queryClient.invalidateQueries(buildAppDataKey(itemId)); - } - }, - }); - const useDeleteAppData = () => - useMutation(MUTATION_KEYS.DELETE_APP_DATA); + const useDeleteAppData = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: { id: AppData['id'] }) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.deleteAppData({ ...data, id: payload.id, apiHost }); + }, + { + onMutate: async (payload) => { + const { itemId } = getDataOrThrow(queryClient); + const prevData = queryClient.getQueryData(buildAppDataKey(itemId)); + if (prevData && itemId) { + queryClient.setQueryData( + buildAppDataKey(itemId), + prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), + ); + } + return prevData; + }, + onSuccess: (prevData) => { + queryConfig?.notifier?.({ type: deleteAppDataRoutine.SUCCESS, payload: prevData }); + }, + onError: (error: Error, _payload, prevData) => { + queryConfig?.notifier?.({ type: deleteAppDataRoutine.FAILURE, payload: { error } }); + if (prevData) { + const { itemId } = getData(queryClient); + const data = queryClient.getQueryData(buildAppDataKey(itemId)); + if (itemId && data) { + queryClient.setQueryData(buildAppDataKey(itemId), prevData); + } + } + }, + onSettled: () => { + const { itemId } = getData(queryClient); + if (itemId && !enableWebsocket) { + queryClient.invalidateQueries(buildAppDataKey(itemId)); + } + }, + }, + ); + }; // this mutation is used for its callback and invalidate the keys /** * @param {UUID} id parent item id wher the file is uploaded in * @param {error} [error] error occured during the file uploading */ - queryClient.setMutationDefaults(MUTATION_KEYS.FILE_UPLOAD, { - mutationFn: async ({ error }) => { - if (error) throw new Error(JSON.stringify(error)); - }, - onSuccess: (_result, { data, error }) => { - if (error) { - throw error; - } else { - notifier?.({ type: uploadAppDataFileRoutine.SUCCESS, payload: { data } }); - } - }, - onError: (_error, { error }) => { - notifier?.({ type: uploadAppDataFileRoutine.FAILURE, payload: { error } }); - }, - onSettled: () => { - const { itemId } = getData(queryClient); - if (itemId) { - queryClient.invalidateQueries(buildAppDataKey(itemId)); - } - }, - }); - const useUploadAppDataFile = () => - useMutation(MUTATION_KEYS.FILE_UPLOAD); + const useUploadAppDataFile = () => { + const queryClient = useQueryClient(); + return useMutation( + async ({ error }: { error?: Error; data?: unknown }) => { + if (error) throw new Error(JSON.stringify(error)); + }, + { + onSuccess: (_result, { data, error }) => { + if (error) { + throw error; + } else { + notifier?.({ type: uploadAppDataFileRoutine.SUCCESS, payload: { data } }); + } + }, + onError: (_error, { error }) => { + notifier?.({ type: uploadAppDataFileRoutine.FAILURE, payload: { error } }); + }, + onSettled: () => { + const { itemId } = getData(queryClient); + if (itemId) { + queryClient.invalidateQueries(buildAppDataKey(itemId)); + } + }, + }, + ); + }; return { usePostAppData, usePatchAppData, useDeleteAppData, useUploadAppDataFile }; }; diff --git a/src/mutations/appSetting.test.ts b/src/mutations/appSetting.test.ts index 311c4951..447d676a 100644 --- a/src/mutations/appSetting.test.ts +++ b/src/mutations/appSetting.test.ts @@ -1,9 +1,7 @@ -import { convertJs } from '@graasp/sdk'; -import { AppSettingRecord } from '@graasp/sdk/frontend'; +import { AppSetting } from '@graasp/sdk'; import { act } from '@testing-library/react'; import { StatusCodes } from 'http-status-codes'; -import { List } from 'immutable'; import nock from 'nock'; import { v4 } from 'uuid'; @@ -21,12 +19,7 @@ import { buildPostAppSettingRoute, } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { - AUTH_TOKEN_KEY, - LOCAL_CONTEXT_KEY, - MUTATION_KEYS, - buildAppSettingsKey, -} from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildAppSettingsKey } from '../config/keys'; import { patchAppSettingRoutine, postAppSettingRoutine } from '../routines'; const mockedNotifier = jest.fn(); @@ -40,7 +33,7 @@ describe('App Settings Mutations', () => { nock.cleanAll(); }); - describe(MUTATION_KEYS.POST_APP_SETTING[0], () => { + describe('usePostAppSetting', () => { const itemId = v4(); const key = buildAppSettingsKey(itemId); const toAdd = buildAppSetting(); @@ -52,11 +45,11 @@ describe('App Settings Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Create app setting', async () => { - queryClient.setQueryData(key, convertJs(initData)); + queryClient.setQueryData(key, initData); const response = toAdd; @@ -80,10 +73,7 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)?.toJS()).toEqual([ - ...initData, - toAdd, - ]); + expect(queryClient.getQueryData(key)).toEqual([...initData, toAdd]); }); }); @@ -91,9 +81,9 @@ describe('App Settings Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); - queryClient.setQueryData(key, convertJs(initData)); + queryClient.setQueryData(key, initData); const response = UNAUTHORIZED_RESPONSE; @@ -123,16 +113,13 @@ describe('App Settings Mutations', () => { }), ); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)?.toJS()).toEqual(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -159,17 +146,17 @@ describe('App Settings Mutations', () => { type: postAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); - // since the itemId is not defined, we do not check data for its key + expect(queryClient.getQueryData(key)).toEqual(initData); + // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -196,13 +183,13 @@ describe('App Settings Mutations', () => { type: postAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); const endpoints = [ @@ -229,19 +216,19 @@ describe('App Settings Mutations', () => { type: postAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); }); - describe(MUTATION_KEYS.PATCH_APP_SETTING[0], () => { - const initData = convertJs(FIXTURE_APP_SETTINGS); + describe('usePatchAppSetting', () => { + const initData = FIXTURE_APP_SETTINGS; const itemId = v4(); - const appDataId = initData.first()?.id ?? v4(); + const appDataId = initData[0]?.id ?? v4(); const key = buildAppSettingsKey(itemId); const toPatch = buildAppSetting({ id: appDataId, data: { new: 'data' } }); - const updatedData = convertJs([toPatch, ...initData.delete(0).toJS()]); + const updatedData = [toPatch, ...initData.slice(1)]; const route = `/${buildPatchAppSettingRoute({ id: toPatch.id, itemId })}`; const mutation = mutations.usePatchAppSetting; @@ -249,7 +236,7 @@ describe('App Settings Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Patch app setting', async () => { @@ -275,10 +262,10 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - const result = queryClient.getQueryData>(key); + const result = queryClient.getQueryData(key); // check data and length - expect(result?.first()?.data?.toJS()).toMatchObject(toPatch.data); - expect(result?.size).toBe(updatedData.size); + expect(result?.[0]?.data).toMatchObject(toPatch.data); + expect(result?.length).toBe(updatedData.length); }); }); @@ -286,7 +273,7 @@ describe('App Settings Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); @@ -318,16 +305,13 @@ describe('App Settings Mutations', () => { }), ); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -354,17 +338,17 @@ describe('App Settings Mutations', () => { type: patchAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); queryClient.setQueryData(key, initData); const endpoints = [ @@ -391,13 +375,13 @@ describe('App Settings Mutations', () => { type: patchAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); queryClient.setQueryData(key, initData); const endpoints = [ @@ -424,13 +408,13 @@ describe('App Settings Mutations', () => { type: patchAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryData(key)).toEqualImmutable(initData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); }); - describe(MUTATION_KEYS.DELETE_APP_SETTING[0], () => { + describe('useDeleteAppSetting', () => { const itemId = v4(); const key = buildAppSettingsKey(itemId); const toDelete = FIXTURE_APP_SETTINGS[0]; @@ -442,11 +426,11 @@ describe('App Settings Mutations', () => { beforeEach(() => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); }); it('Delete app data', async () => { - const initData = convertJs([toDelete, FIXTURE_APP_SETTINGS[1]]); + const initData = [toDelete, FIXTURE_APP_SETTINGS[1]]; queryClient.setQueryData(key, initData); const endpoints = [ @@ -469,9 +453,7 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable( - convertJs([FIXTURE_APP_SETTINGS[1]]), - ); + expect(queryClient.getQueryData(key)).toEqual([FIXTURE_APP_SETTINGS[1]]); }); }); @@ -479,9 +461,9 @@ describe('App Settings Mutations', () => { it('Unauthorized', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); - queryClient.setQueryData(key, convertJs([toDelete])); + queryClient.setQueryData(key, [toDelete]); const response = UNAUTHORIZED_RESPONSE; @@ -506,21 +488,16 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData>(key)).toEqualImmutable( - convertJs([toDelete]), - ); + expect(queryClient.getQueryData(key)).toEqual([toDelete]); }); it('Throw if itemId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext(), itemId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { ...buildMockLocalContext(), itemId: null }); - const initialData = convertJs([toDelete]); - queryClient.setQueryData(key, initialData); + const initData = [toDelete]; + queryClient.setQueryData(key, initData); const endpoints = [ { @@ -541,20 +518,20 @@ describe('App Settings Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initialData); + expect(queryClient.getQueryData(key)).toEqual(initData); // since the itemid is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { // set necessary data queryClient.setQueryData(AUTH_TOKEN_KEY, MOCK_TOKEN); - queryClient.setQueryData( - LOCAL_CONTEXT_KEY, - convertJs({ ...buildMockLocalContext({ itemId }), memberId: null }), - ); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, { + ...buildMockLocalContext({ itemId }), + memberId: null, + }); - const initialData = convertJs([toDelete]); - queryClient.setQueryData(key, initialData); + const initData = [toDelete]; + queryClient.setQueryData(key, initData); const endpoints = [ { @@ -575,16 +552,16 @@ describe('App Settings Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initialData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if token is undefined', async () => { // set necessary data - queryClient.setQueryData(LOCAL_CONTEXT_KEY, convertJs(buildMockLocalContext({ itemId }))); + queryClient.setQueryData(LOCAL_CONTEXT_KEY, buildMockLocalContext({ itemId })); - const initialData = convertJs([toDelete]); - queryClient.setQueryData(key, initialData); + const initData = [toDelete]; + queryClient.setQueryData(key, initData); const endpoints = [ { @@ -605,7 +582,7 @@ describe('App Settings Mutations', () => { await waitForMutation(); }); - expect(queryClient.getQueryData(key)).toEqualImmutable(initialData); + expect(queryClient.getQueryData(key)).toEqual(initData); expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); }); }); diff --git a/src/mutations/appSetting.ts b/src/mutations/appSetting.ts index 09b13841..b6b00533 100644 --- a/src/mutations/appSetting.ts +++ b/src/mutations/appSetting.ts @@ -1,11 +1,9 @@ -import { AppSetting, convertJs } from '@graasp/sdk'; -import { AppSettingRecord } from '@graasp/sdk/frontend'; +import { AppSetting } from '@graasp/sdk'; -import { QueryClient, useMutation } from '@tanstack/react-query'; -import { List } from 'immutable'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { MUTATION_KEYS, buildAppSettingsKey } from '../config/keys'; +import { buildAppSettingsKey } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { deleteAppSettingRoutine, @@ -15,163 +13,166 @@ import { } from '../routines'; import { QueryClientConfig } from '../types'; -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => { +export default (queryConfig: QueryClientConfig) => { const { notifier, enableWebsocket } = queryConfig; - queryClient.setMutationDefaults(MUTATION_KEYS.POST_APP_SETTING, { - mutationFn: (payload: Partial) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.postAppSetting({ ...data, body: payload, apiHost }); - }, - onSuccess: (newAppSetting: AppSetting) => { - const { itemId } = getData(queryClient); - const key = buildAppSettingsKey(itemId); - const prevData = queryClient.getQueryData>(key); - const newData: AppSettingRecord = convertJs(newAppSetting); - if (!prevData) { - queryClient.setQueryData(key, List.of(newData)); - } else if (!prevData.some((a) => a.id === newData.id)) { - queryClient.setQueryData(key, prevData?.push(newData)); - } - queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newData }); - }, - onError: (error) => { - queryConfig?.notifier?.({ type: postAppSettingRoutine.FAILURE, payload: { error } }); - }, - onSettled: () => { - if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); - } - }, - }); - const usePostAppSetting = () => - useMutation>(MUTATION_KEYS.POST_APP_SETTING); + const usePostAppSetting = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: Partial) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.postAppSetting({ ...data, body: payload, apiHost }); + }, + { + onSuccess: (newAppSetting: AppSetting) => { + const { itemId } = getData(queryClient); + const key = buildAppSettingsKey(itemId); + const prevData = queryClient.getQueryData(key); + const newData: AppSetting = newAppSetting; + if (!prevData) { + queryClient.setQueryData(key, newData); + } else if (!prevData.some((a) => a.id === newData.id)) { + queryClient.setQueryData(key, [...(prevData ?? []), newData]); + } + queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newData }); + }, + onError: (error: Error) => { + queryConfig?.notifier?.({ type: postAppSettingRoutine.FAILURE, payload: { error } }); + }, + onSettled: () => { + if (!enableWebsocket) { + const { itemId } = getData(queryClient); + queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + } + }, + }, + ); + }; - queryClient.setMutationDefaults(MUTATION_KEYS.PATCH_APP_SETTING, { - mutationFn: (payload: Partial & { id: string }) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.patchAppSetting({ ...data, id: payload.id, data: payload.data, apiHost }); - }, - onMutate: async (payload) => { - let context = null; - const { itemId } = getData(queryClient); - const prevData = queryClient.getQueryData>( - buildAppSettingsKey(itemId), - ); - if (itemId && prevData) { - const newData = prevData.map((appData) => - appData.id === payload.id ? appData.merge(convertJs(payload)) : appData, - ); - queryClient.setQueryData(buildAppSettingsKey(itemId), newData); - context = prevData; - } - return context; - }, - onSuccess: (newData) => { - queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newData }); - }, - onError: (error, _payload, prevData) => { - queryConfig?.notifier?.({ type: patchAppSettingRoutine.FAILURE, payload: { error } }); + const usePatchAppSetting = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: Partial & { id: string }) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.patchAppSetting({ ...data, id: payload.id, data: payload.data, apiHost }); + }, + { + onMutate: async (payload) => { + let context; + const { itemId } = getData(queryClient); + const prevData = queryClient.getQueryData(buildAppSettingsKey(itemId)); + if (itemId && prevData) { + const newData = prevData.map((appData) => + appData.id === payload.id ? { ...appData, ...payload } : appData, + ); + queryClient.setQueryData(buildAppSettingsKey(itemId), newData); + context = prevData; + } + return context; + }, + onSuccess: (newData) => { + queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newData }); + }, + onError: (error: Error, _payload, prevData) => { + queryConfig?.notifier?.({ type: patchAppSettingRoutine.FAILURE, payload: { error } }); - if (prevData) { - const { itemId } = getData(queryClient); - const data = queryClient.getQueryData>(buildAppSettingsKey(itemId)); - if (itemId && data) { - queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); - } - } - }, - onSettled: () => { - if (!enableWebsocket) { - const data = getData(queryClient); - queryClient.invalidateQueries(buildAppSettingsKey(data?.itemId)); - } - }, - }); - const usePatchAppSetting = () => - useMutation & { id: string }>( - MUTATION_KEYS.PATCH_APP_SETTING, + if (prevData) { + const { itemId } = getData(queryClient); + const data = queryClient.getQueryData(buildAppSettingsKey(itemId)); + if (itemId && data) { + queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); + } + } + }, + onSettled: () => { + if (!enableWebsocket) { + const data = getData(queryClient); + queryClient.invalidateQueries(buildAppSettingsKey(data?.itemId)); + } + }, + }, ); + }; - queryClient.setMutationDefaults(MUTATION_KEYS.DELETE_APP_SETTING, { - mutationFn: (payload: { id: string }) => { - const apiHost = getApiHost(queryClient); - const data = getDataOrThrow(queryClient); - return Api.deleteAppSetting({ ...data, id: payload.id, apiHost }); - }, - onMutate: async (payload) => { - const { itemId } = getDataOrThrow(queryClient); - const prevData = queryClient.getQueryData>( - buildAppSettingsKey(itemId), - ); - if (prevData && itemId) { - queryClient.setQueryData( - buildAppSettingsKey(itemId), - prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), - ); - } - return prevData; - }, - onSuccess: (prevData) => { - queryConfig?.notifier?.({ type: deleteAppSettingRoutine.SUCCESS, payload: prevData }); - }, - onError: (error, _payload, prevData) => { - queryConfig?.notifier?.({ type: deleteAppSettingRoutine.FAILURE, payload: { error } }); + const useDeleteAppSetting = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: { id: string }) => { + const apiHost = getApiHost(queryClient); + const data = getDataOrThrow(queryClient); + return Api.deleteAppSetting({ ...data, id: payload.id, apiHost }); + }, + { + onMutate: async (payload) => { + const { itemId } = getDataOrThrow(queryClient); + const prevData = queryClient.getQueryData(buildAppSettingsKey(itemId)); + if (prevData && itemId) { + queryClient.setQueryData( + buildAppSettingsKey(itemId), + prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), + ); + } + return prevData; + }, + onSuccess: (prevData) => { + queryConfig?.notifier?.({ type: deleteAppSettingRoutine.SUCCESS, payload: prevData }); + }, + onError: (error: Error, _payload, prevData) => { + queryConfig?.notifier?.({ type: deleteAppSettingRoutine.FAILURE, payload: { error } }); - if (prevData) { - const { itemId } = getData(queryClient); - const data = queryClient.getQueryData>(buildAppSettingsKey(itemId)); - if (itemId && data) { - queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); - } - } - }, - onSettled: () => { - if (!enableWebsocket) { - const { itemId } = getData(queryClient); - if (itemId) { - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); - } - } - }, - }); - const useDeleteAppSetting = () => - useMutation(MUTATION_KEYS.DELETE_APP_SETTING); + if (prevData) { + const { itemId } = getData(queryClient); + const data = queryClient.getQueryData(buildAppSettingsKey(itemId)); + if (itemId && data) { + queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); + } + } + }, + onSettled: () => { + if (!enableWebsocket) { + const { itemId } = getData(queryClient); + if (itemId) { + queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + } + } + }, + }, + ); + }; // this mutation is used for its callback and invalidate the keys /** * @param {UUID} id parent item id wher the file is uploaded in * @param {error} [error] error occured during the file uploading */ - queryClient.setMutationDefaults(MUTATION_KEYS.APP_SETTING_FILE_UPLOAD, { - mutationFn: async ({ error }) => { - if (error) throw new Error(JSON.stringify(error)); - }, - onSuccess: (_result, { data, error }) => { - if (error) { - throw error; - } else { - notifier?.({ type: uploadAppSettingFileRoutine.SUCCESS, payload: { data } }); - } - }, - onError: (_error, { error }) => { - notifier?.({ type: uploadAppSettingFileRoutine.FAILURE, payload: { error } }); - }, - onSettled: () => { - const { itemId } = getData(queryClient); - if (itemId) { - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); - } - }, - }); - const useUploadAppSettingFile = () => - useMutation( - MUTATION_KEYS.APP_SETTING_FILE_UPLOAD, + const useUploadAppSettingFile = () => { + const queryClient = useQueryClient(); + return useMutation( + async ({ error }: { data?: unknown; error?: Error }) => { + if (error) throw new Error(JSON.stringify(error)); + }, + { + onSuccess: (_result, { data, error }) => { + if (error) { + throw error; + } else { + notifier?.({ type: uploadAppSettingFileRoutine.SUCCESS, payload: { data } }); + } + }, + onError: (_error, { error }) => { + notifier?.({ type: uploadAppSettingFileRoutine.FAILURE, payload: { error } }); + }, + onSettled: () => { + const { itemId } = getData(queryClient); + if (itemId) { + queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + } + }, + }, ); + }; return { usePostAppSetting, usePatchAppSetting, useDeleteAppSetting, useUploadAppSettingFile }; }; diff --git a/src/mutations/index.ts b/src/mutations/index.ts index 3c7fe3c1..6ff39876 100644 --- a/src/mutations/index.ts +++ b/src/mutations/index.ts @@ -1,15 +1,13 @@ -import { QueryClient } from '@tanstack/react-query'; - import { QueryClientConfig } from '../types'; import appActionMutations from './appAction'; import appMutations from './appData'; import appSettingMutations from './appSetting'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const configureMutations = (queryClient: QueryClient, queryConfig: QueryClientConfig) => ({ - ...appMutations(queryClient, queryConfig), - ...appSettingMutations(queryClient, queryConfig), - ...appActionMutations(queryClient, queryConfig), +const configureMutations = (queryConfig: QueryClientConfig) => ({ + ...appMutations(queryConfig), + ...appSettingMutations(queryConfig), + ...appActionMutations(queryConfig), }); export default configureMutations; diff --git a/src/queryClient.ts b/src/queryClient.ts index e9919546..a404af54 100644 --- a/src/queryClient.ts +++ b/src/queryClient.ts @@ -11,7 +11,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { API_ROUTES } from './api/routes'; import { CACHE_TIME_MILLISECONDS, STALE_TIME_MILLISECONDS } from './config/constants'; -import { MUTATION_KEYS, QUERY_KEYS, buildPostMessageKeys } from './config/keys'; +import { QUERY_KEYS, buildPostMessageKeys } from './config/keys'; import configureHooks from './hooks'; import configureMutations from './mutations'; import { OptionalQueryClientConfig, QueryClientConfig, RequiredQueryClientConfig } from './types'; @@ -48,7 +48,6 @@ const configure = ( Hydrate: typeof Hydrate; hooks: ReturnType; mutations: ReturnType; - MUTATION_KEYS: typeof MUTATION_KEYS; QUERY_KEYS: typeof QUERY_KEYS; queryClient: QueryClient; QueryClientProvider: typeof QueryClientProvider; @@ -89,7 +88,7 @@ const configure = ( // set up mutations given config // mutations are attached to queryClient - const mutations = configureMutations(queryClient, queryConfig); + const mutations = configureMutations(queryConfig); // set up websocket client const websocketClient = getWebsocketClient(queryConfig); @@ -104,7 +103,6 @@ const configure = ( dehydrate, hooks, Hydrate, - MUTATION_KEYS, mutations, QUERY_KEYS, queryClient, diff --git a/src/routines/appAction.ts b/src/routines/appAction.ts index 88b5fc3d..54578f24 100644 --- a/src/routines/appAction.ts +++ b/src/routines/appAction.ts @@ -1,4 +1,3 @@ -import { MUTATION_KEYS } from '../config/keys'; import createRoutine from './utils'; -export const postAppActionRoutine = createRoutine(MUTATION_KEYS.POST_APP_ACTION[0]); +export const postAppActionRoutine = createRoutine('POST_APP_ACTION'); diff --git a/src/types.ts b/src/types.ts index c6e0ff3e..8f7ce803 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,7 +10,6 @@ import { PermissionLevel, UUID, } from '@graasp/sdk'; -import { ImmutableCast } from '@graasp/sdk/frontend'; // generic type type EnumToUnionType = T extends `${infer R}` ? R : never; @@ -74,16 +73,11 @@ export type LocalContext = { permission: PermissionLevel; }; -export type LocalContextRecord = ImmutableCast; - -export type AppContext = { - item: AppItemType; +export type AppContext = DiscriminatedItem & { children: DiscriminatedItem[]; members: Member[]; }; -export type AppContextRecord = ImmutableCast; - export interface ApiData { token: Token; itemId: UUID; diff --git a/src/ws/hooks/app.test.ts b/src/ws/hooks/app.test.ts index 08dabdff..3a2f5529 100644 --- a/src/ws/hooks/app.test.ts +++ b/src/ws/hooks/app.test.ts @@ -1,7 +1,4 @@ -import { convertJs } from '@graasp/sdk'; -import { AppActionRecord, AppDataRecord, AppSettingRecord } from '@graasp/sdk/frontend'; - -import { List } from 'immutable'; +import { AppAction, AppData, AppSetting } from '@graasp/sdk'; import { FIXTURE_APP_ACTIONS, @@ -41,7 +38,7 @@ describe('Websockets App Hooks', () => { describe('useAppDataUpdates', () => { const appDataArray = FIXTURE_APP_DATA; - const appDataList: List = convertJs(appDataArray); + const appDataList: AppData[] = appDataArray; const itemId = FIXTURE_CONTEXT.id; const appDataKey = buildAppDataKey(itemId); const channel = { name: itemId, topic: APP_DATA_TOPIC }; @@ -53,7 +50,7 @@ describe('Websockets App Hooks', () => { mockWsHook({ hook, wrapper, enabled: true }); expect(handlers.length).toBeGreaterThan(0); expect(queryClient).toBeDefined(); - expect(queryClient.getQueryData(appDataKey)).toEqualImmutable(appDataList); + expect(queryClient.getQueryData(appDataKey)).toEqual(appDataList); }); it('Receives post app data', async () => { @@ -72,10 +69,8 @@ describe('Websockets App Hooks', () => { h?.handler(appDataEvent); expect( - queryClient - .getQueryData>(appDataKey) - ?.find((a) => a.id === newAppData.id), - ).toEqualImmutable(convertJs(newAppData)); + queryClient.getQueryData(appDataKey)?.find((a) => a.id === newAppData.id), + ).toEqual(newAppData); }); it('Receives patch app data', async () => { @@ -92,12 +87,9 @@ describe('Websockets App Hooks', () => { }; getHandlerByChannel(handlers, channel)?.handler(appDataEvent); - expect( - queryClient - .getQueryData>(appDataKey) - ?.find((a) => a.id === newAppData.id), - ).toEqualImmutable(convertJs(newAppData)); + queryClient.getQueryData(appDataKey)?.find((a) => a.id === newAppData.id), + ).toEqual(newAppData); }); it('Receives delete app data', async () => { @@ -107,10 +99,8 @@ describe('Websockets App Hooks', () => { const newAppData = appDataArray[1]; // Doesn't work with same app data than other tests (index 0) expect( - queryClient - .getQueryData>(appDataKey) - ?.find((a) => a.id === newAppData.id), - ).toEqualImmutable(convertJs(newAppData)); + queryClient.getQueryData(appDataKey)?.find((a) => a.id === newAppData.id), + ).toEqual(newAppData); const appDataEvent: AppDataEvent = { kind: AppEventKinds.AppData, @@ -121,24 +111,20 @@ describe('Websockets App Hooks', () => { getHandlerByChannel(handlers, channel)?.handler(appDataEvent); expect( - queryClient - .getQueryData>(appDataKey) - ?.find((a) => a.id === newAppData.id), + queryClient.getQueryData(appDataKey)?.find((a) => a.id === newAppData.id), ).toBeUndefined(); }); it('Receives delete app data, while having a single app data remaining', async () => { const lastAppData = appDataArray[1]; - const singleAppDataList: List = convertJs([lastAppData]); + const singleAppDataList: AppData[] = [lastAppData]; queryClient.setQueryData(appDataKey, singleAppDataList); - expect(singleAppDataList.size).toEqual(1); + expect(singleAppDataList.length).toEqual(1); await mockWsHook({ hook, wrapper }); expect( - queryClient - .getQueryData>(appDataKey) - ?.find((a) => a.id === lastAppData.id), - ).toEqualImmutable(convertJs(lastAppData)); + queryClient.getQueryData(appDataKey)?.find((a) => a.id === lastAppData.id), + ).toEqual(lastAppData); const appDataEvent: AppDataEvent = { kind: AppEventKinds.AppData, @@ -148,15 +134,15 @@ describe('Websockets App Hooks', () => { getHandlerByChannel(handlers, channel)?.handler(appDataEvent); - const queryData = queryClient.getQueryData>(appDataKey); - expect(queryData?.size).toEqual(0); + const queryData = queryClient.getQueryData(appDataKey); + expect(queryData?.length).toEqual(0); expect(queryData?.find((a) => a.id === lastAppData.id)).toBeUndefined(); }); }); describe('useAppActionsUpdates', () => { const appActionsArray = FIXTURE_APP_ACTIONS; - const appActionsList: List = convertJs(appActionsArray); + const appActionsList: AppAction[] = appActionsArray; const itemId = FIXTURE_CONTEXT.id; const appActionsKey = buildAppActionsKey(itemId); const channel = { name: itemId, topic: APP_ACTIONS_TOPIC }; @@ -168,7 +154,7 @@ describe('Websockets App Hooks', () => { mockWsHook({ hook, wrapper, enabled: true }); expect(handlers.length).toBeGreaterThan(0); expect(queryClient).toBeDefined(); - expect(queryClient.getQueryData(appActionsKey)).toEqualImmutable(appActionsList); + expect(queryClient.getQueryData(appActionsKey)).toEqual(appActionsList); }); it('Receives post app action', async () => { @@ -187,16 +173,14 @@ describe('Websockets App Hooks', () => { h?.handler(appActionEvent); expect( - queryClient - .getQueryData>(appActionsKey) - ?.find((a) => a.id === newAppAction.id), - ).toEqualImmutable(convertJs(newAppAction)); + queryClient.getQueryData(appActionsKey)?.find((a) => a.id === newAppAction.id), + ).toEqual(newAppAction); }); }); describe('useAppSettingsUpdates', () => { const appSettingsArray = FIXTURE_APP_SETTINGS; - const appSettingsList: List = convertJs(appSettingsArray); + const appSettingsList: AppSetting[] = appSettingsArray; const itemId = FIXTURE_CONTEXT.id; const appSettingsKey = buildAppSettingsKey(itemId); const channel = { name: itemId, topic: APP_SETTINGS_TOPIC }; @@ -208,7 +192,7 @@ describe('Websockets App Hooks', () => { mockWsHook({ hook, wrapper, enabled: true }); expect(handlers.length).toBeGreaterThan(0); expect(queryClient).toBeDefined(); - expect(queryClient.getQueryData(appSettingsKey)).toEqualImmutable(appSettingsList); + expect(queryClient.getQueryData(appSettingsKey)).toEqual(appSettingsList); }); it('Receives post app setting', async () => { @@ -225,12 +209,9 @@ describe('Websockets App Hooks', () => { const h = getHandlerByChannel(handlers, channel); h?.handler(appDataEvent); - expect( - queryClient - .getQueryData>(appSettingsKey) - ?.find((a) => a.id === newAppSetting.id), - ).toEqualImmutable(convertJs(newAppSetting)); + queryClient.getQueryData(appSettingsKey)?.find((a) => a.id === newAppSetting.id), + ).toEqual(newAppSetting); }); it('Receives patch app setting', async () => { @@ -249,10 +230,8 @@ describe('Websockets App Hooks', () => { getHandlerByChannel(handlers, channel)?.handler(appDataEvent); expect( - queryClient - .getQueryData>(appSettingsKey) - ?.find((a) => a.id === newAppSetting.id), - ).toEqualImmutable(convertJs(newAppSetting)); + queryClient.getQueryData(appSettingsKey)?.find((a) => a.id === newAppSetting.id), + ).toEqual(newAppSetting); }); it('Receives delete app setting', async () => { @@ -262,10 +241,8 @@ describe('Websockets App Hooks', () => { const newAppSetting = appSettingsArray[1]; // Doesn't work with same app data than other tests (index 0) expect( - queryClient - .getQueryData>(appSettingsKey) - ?.find((a) => a.id === newAppSetting.id), - ).toEqualImmutable(convertJs(newAppSetting)); + queryClient.getQueryData(appSettingsKey)?.find((a) => a.id === newAppSetting.id), + ).toEqual(newAppSetting); const appDataEvent: AppSettingEvent = { kind: AppEventKinds.AppSettings, @@ -276,9 +253,7 @@ describe('Websockets App Hooks', () => { getHandlerByChannel(handlers, channel)?.handler(appDataEvent); expect( - queryClient - .getQueryData>(appSettingsKey) - ?.find((a) => a.id === newAppSetting.id), + queryClient.getQueryData(appSettingsKey)?.find((a) => a.id === newAppSetting.id), ).toBeUndefined(); }); }); diff --git a/src/ws/hooks/app.ts b/src/ws/hooks/app.ts index eb766034..a0344d33 100644 --- a/src/ws/hooks/app.ts +++ b/src/ws/hooks/app.ts @@ -4,11 +4,9 @@ */ import { useEffect } from 'react'; -import { UUID, convertJs } from '@graasp/sdk'; -import { AppActionRecord, AppDataRecord, AppSettingRecord } from '@graasp/sdk/frontend'; +import { AppAction, AppData, AppSetting, UUID } from '@graasp/sdk'; import { useQueryClient } from '@tanstack/react-query'; -import { List } from 'immutable'; import { APP_ACTIONS_TOPIC, APP_DATA_TOPIC, APP_SETTINGS_TOPIC } from '../../config/constants'; import { buildAppActionsKey, buildAppDataKey, buildAppSettingsKey } from '../../config/keys'; @@ -46,31 +44,34 @@ export const configureWsAppDataHooks = (websocketClient?: WebsocketClient) => ({ const handler = (event: AppDataEvent): void => { if (event.kind === AppEventKinds.AppData) { - const appDataList: List | undefined = queryClient.getQueryData(appDataKey); - const newAppData: AppDataRecord = convertJs(event.appData); + const appDataList: AppData[] | undefined = queryClient.getQueryData(appDataKey); + const newAppData: AppData = event.appData; switch (event.op) { case AppOperations.POST: { if (!appDataList?.some(({ id }) => id === newAppData.id)) queryClient.setQueryData( appDataKey, - appDataList ? appDataList.push(newAppData) : List.of(newAppData), + appDataList ? [...(appDataList ?? []), newAppData] : [newAppData], ); break; } case AppOperations.PATCH: { - const appDataPatchedIndex = appDataList?.findIndex((a) => a.id === newAppData.id); - if (typeof appDataPatchedIndex !== 'undefined' && appDataPatchedIndex >= 0) { - queryClient.setQueryData( - appDataKey, - appDataList?.set(appDataPatchedIndex, newAppData), - ); + if (appDataList) { + const appDataPatchedIndex = appDataList.findIndex((a) => a.id === newAppData.id); + if (appDataPatchedIndex >= 0) { + queryClient.setQueryData(appDataKey, [ + ...appDataList.slice(0, appDataPatchedIndex), + newAppData, + ...appDataList.slice(appDataPatchedIndex, appDataList.length), + ]); + } } break; } case AppOperations.DELETE: { queryClient.setQueryData( appDataKey, - appDataList?.filterNot(({ id }) => id === newAppData.id), + appDataList?.filter(({ id }) => id !== newAppData.id), ); break; } @@ -115,15 +116,14 @@ export const configureWsAppActionsHooks = (websocketClient?: WebsocketClient) => const handler = (event: AppActionEvent): void => { if (event.kind === AppEventKinds.AppActions) { - const appActionsList: List | undefined = - queryClient.getQueryData(appActionKey); - const newAppAction: AppActionRecord = convertJs(event.appAction); + const appActionsList: AppAction[] | undefined = queryClient.getQueryData(appActionKey); + const newAppAction: AppAction = event.appAction; switch (event.op) { case AppOperations.POST: { if (!appActionsList?.some(({ id }) => id === newAppAction.id)) { queryClient.setQueryData( appActionKey, - appActionsList ? appActionsList.push(newAppAction) : List.of(newAppAction), + appActionsList ? [...(appActionsList ?? []), newAppAction] : [newAppAction], ); } break; @@ -169,35 +169,37 @@ export const configureWsAppSettingHooks = (websocketClient?: WebsocketClient) => const handler = (event: AppSettingEvent): void => { if (event.kind === AppEventKinds.AppSettings) { - const appSettingList: List | undefined = - queryClient.getQueryData(appSettingsKey); - const newAppSetting: AppSettingRecord = convertJs(event.appSetting); + const appSettingList: AppSetting[] | undefined = queryClient.getQueryData(appSettingsKey); + const newAppSetting: AppSetting = event.appSetting; switch (event.op) { case AppOperations.POST: { if (!appSettingList?.some(({ id }) => id === newAppSetting.id)) { queryClient.setQueryData( appSettingsKey, - appSettingList ? appSettingList.push(newAppSetting) : List.of(newAppSetting), + appSettingList ? [...(appSettingList ?? []), newAppSetting] : [newAppSetting], ); } break; } case AppOperations.PATCH: { - const appSettingPatchedIndex = appSettingList?.findIndex( - (a) => a.id === newAppSetting.id, - ); - if (typeof appSettingPatchedIndex !== 'undefined' && appSettingPatchedIndex >= 0) { - queryClient.setQueryData( - appSettingsKey, - appSettingList?.set(appSettingPatchedIndex, newAppSetting), + if (appSettingList) { + const appSettingPatchedIndex = appSettingList.findIndex( + (a) => a.id === newAppSetting.id, ); + if (appSettingPatchedIndex >= 0) { + queryClient.setQueryData(appSettingsKey, [ + ...appSettingList.slice(0, appSettingPatchedIndex), + newAppSetting, + ...appSettingList.slice(appSettingPatchedIndex, appSettingList.length), + ]); + } } break; } case AppOperations.DELETE: { queryClient.setQueryData( appSettingsKey, - appSettingList?.filterNot(({ id }) => id === newAppSetting.id), + appSettingList?.filter(({ id }) => id !== newAppSetting.id), ); break; } diff --git a/test/setupTestsAfterEnv.ts b/test/setupTestsAfterEnv.ts deleted file mode 100644 index 5be1e3dd..00000000 --- a/test/setupTestsAfterEnv.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as matchers from 'jest-immutable-matchers'; - -global.beforeEach(() => { - expect.extend(matchers); -}); diff --git a/yarn.lock b/yarn.lock index 6c86b05b..9c21ca8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2273,7 +2273,6 @@ __metadata: eslint-plugin-react-hooks: "npm:4.6.0" http-status-codes: "npm:2.3.0" husky: "npm:8.0.3" - immutable: "npm:4.3.4" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" jest-immutable-matchers: "npm:3.0.0"