diff --git a/package.json b/package.json index 30c9e0aa..076fec2c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@eslint/eslintrc": "3.1.0", "@eslint/js": "9.10.0", "@graasp/sdk": "4.29.1", - "@graasp/translations": "1.37.1", + "@graasp/translations": "1.38.0", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", diff --git a/src/hooks/index.ts b/src/hooks/index.ts index b2cd77df..21d38fdb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -5,6 +5,7 @@ import configureItemPublicationHooks from '../item/publication/hooks.js'; import configureMemberHooks from '../member/hooks.js'; import configurePublicProfileHooks from '../member/publicProfile/hooks.js'; import configureSubscriptionHooks from '../member/subscription/hooks.js'; +import configureMembershipRequestHooks from '../membership/request/hooks.js'; import { QueryClientConfig } from '../types.js'; import configureActionHooks from './action.js'; import configureAppsHooks from './apps.js'; @@ -60,6 +61,7 @@ export default ( ...configureItemGeolocationHooks(queryConfig), ...configureEmbeddedLinkHooks(queryConfig), ...configureItemPublicationHooks(queryConfig), + ...configureMembershipRequestHooks(queryConfig), useDebounce, }; }; diff --git a/src/hooks/invitation.ts b/src/hooks/invitation.ts index d37d1944..1ffe69ba 100644 --- a/src/hooks/invitation.ts +++ b/src/hooks/invitation.ts @@ -27,7 +27,10 @@ export default (queryConfig: QueryClientConfig) => { }, }); - const useItemInvitations = (itemId?: UUID) => + const useItemInvitations = ( + itemId?: UUID, + options: { enabled?: boolean } = {}, + ) => useQuery({ queryKey: itemKeys.single(itemId).invitation, queryFn: () => { @@ -37,7 +40,7 @@ export default (queryConfig: QueryClientConfig) => { return Api.getInvitationsForItem(itemId, queryConfig); }, - enabled: Boolean(itemId), + enabled: Boolean(itemId) && (options?.enabled ?? true), ...defaultQueryOptions, }); diff --git a/src/item/itemLogin/api.ts b/src/item/itemLogin/api.ts new file mode 100644 index 00000000..d1dfc004 --- /dev/null +++ b/src/item/itemLogin/api.ts @@ -0,0 +1,13 @@ +import { DiscriminatedItem } from '@graasp/sdk'; + +import { verifyAuthentication } from '../../api/axios.js'; +import { PartialQueryConfigForApi } from '../../types.js'; +import { buildEnroll } from './routes.js'; + +export const enroll = async ( + { itemId }: { itemId: DiscriminatedItem['id'] }, + { API_HOST, axios }: PartialQueryConfigForApi, +) => + verifyAuthentication(() => { + return axios.post(`${API_HOST}/${buildEnroll(itemId)}`); + }); diff --git a/src/item/itemLogin/mutations.test.ts b/src/item/itemLogin/mutations.test.ts new file mode 100644 index 00000000..701c079f --- /dev/null +++ b/src/item/itemLogin/mutations.test.ts @@ -0,0 +1,106 @@ +import { FolderItemFactory, HttpMethod } from '@graasp/sdk'; +import { SUCCESS_MESSAGES } from '@graasp/translations'; + +import { act } from '@testing-library/react'; +import { StatusCodes } from 'http-status-codes'; +import nock from 'nock'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { OK_RESPONSE, UNAUTHORIZED_RESPONSE } from '../../../test/constants.js'; +import { + mockMutation, + setUpTest, + waitForMutation, +} from '../../../test/utils.js'; +import { itemKeys } from '../../keys.js'; +import { buildEnroll } from './routes.js'; +import { enrollRoutine } from './routines.js'; + +const mockedNotifier = vi.fn(); +const { wrapper, queryClient, mutations } = setUpTest({ + notifier: mockedNotifier, +}); + +const item = FolderItemFactory(); +const itemId = item.id; +const key = itemKeys.single(itemId).content; +const membershipKey = itemKeys.single(itemId).memberships; + +describe('useEnroll', () => { + const mutation = mutations.useEnroll; + const route = `/${buildEnroll(itemId)}`; + + afterEach(() => { + nock.cleanAll(); + queryClient.clear(); + }); + + it('Enroll', async () => { + // set data in cache + queryClient.setQueryData(key, item); + queryClient.setQueryData(membershipKey, [{}]); + + const endpoints = [ + { + response: OK_RESPONSE, + method: HttpMethod.Post, + route, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ itemId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith({ + type: enrollRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.ENROLL }, + }); + + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); + expect( + queryClient.getQueryState(membershipKey)?.isInvalidated, + ).toBeTruthy(); + }); + + it('Unauthorized to enroll', async () => { + // set data in cache + queryClient.setQueryData(key, item); + queryClient.setQueryData(membershipKey, [{}]); + + const endpoints = [ + { + response: UNAUTHORIZED_RESPONSE, + statusCode: StatusCodes.UNAUTHORIZED, + method: HttpMethod.Patch, + route, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ itemId }); + await waitForMutation(); + }); + + const state = queryClient.getQueryState(key); + expect(state?.isInvalidated).toBeTruthy(); + + expect(mockedNotifier).toHaveBeenCalledWith({ + type: enrollRoutine.FAILURE, + payload: expect.anything(), + }); + }); +}); diff --git a/src/item/itemLogin/mutations.ts b/src/item/itemLogin/mutations.ts new file mode 100644 index 00000000..e74f5207 --- /dev/null +++ b/src/item/itemLogin/mutations.ts @@ -0,0 +1,37 @@ +import { UUID } from '@graasp/sdk'; +import { SUCCESS_MESSAGES } from '@graasp/translations'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { itemKeys } from '../../keys.js'; +import { QueryClientConfig } from '../../types.js'; +import { enroll } from './api.js'; +import { enrollRoutine } from './routines.js'; + +export const useEnroll = (queryConfig: QueryClientConfig) => () => { + const { notifier } = queryConfig; + + const queryClient = useQueryClient(); + return useMutation( + (payload: { itemId: UUID }) => enroll(payload, queryConfig), + { + onSuccess: () => { + notifier?.({ + type: enrollRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.ENROLL }, + }); + }, + onError: (error: Error, _args, _context) => { + notifier?.({ + type: enrollRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { itemId }) => { + // on success, enroll should have given membership to the user + // invalidate full item because of packed + queryClient.invalidateQueries(itemKeys.single(itemId).content); + }, + }, + ); +}; diff --git a/src/item/itemLogin/routes.ts b/src/item/itemLogin/routes.ts new file mode 100644 index 00000000..a1821919 --- /dev/null +++ b/src/item/itemLogin/routes.ts @@ -0,0 +1,6 @@ +import { DiscriminatedItem } from '@graasp/sdk'; + +import { ITEMS_ROUTE } from '../routes.js'; + +export const buildEnroll = (itemId: DiscriminatedItem['id']) => + `${ITEMS_ROUTE}/${itemId}/enroll`; diff --git a/src/item/itemLogin/routines.ts b/src/item/itemLogin/routines.ts new file mode 100644 index 00000000..8a1fdf6d --- /dev/null +++ b/src/item/itemLogin/routines.ts @@ -0,0 +1,3 @@ +import createRoutine from '../../routines/utils.js'; + +export const enrollRoutine = createRoutine('ENROLL'); diff --git a/src/item/reorder/mutations.test.ts b/src/item/reorder/mutations.test.ts index f2008b6b..abe2d106 100644 --- a/src/item/reorder/mutations.test.ts +++ b/src/item/reorder/mutations.test.ts @@ -3,8 +3,9 @@ import { SUCCESS_MESSAGES } from '@graasp/translations'; import { act } from '@testing-library/react'; import { StatusCodes } from 'http-status-codes'; +import nock from 'nock'; import { v4 } from 'uuid'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { UNAUTHORIZED_RESPONSE } from '../../../test/constants.js'; import { @@ -29,6 +30,11 @@ describe('useReorderItem', () => { const mutation = mutations.useReorderItem; const { id: parentItemId } = FolderItemFactory(); + afterEach(() => { + nock.cleanAll(); + queryClient.clear(); + }); + it('Reorder item', async () => { const route = `/${buildReorderItemRoute({ id: child.id })}`; diff --git a/src/membership/request/api.ts b/src/membership/request/api.ts new file mode 100644 index 00000000..ee79d46e --- /dev/null +++ b/src/membership/request/api.ts @@ -0,0 +1,51 @@ +import { + CompleteMembershipRequest, + Member, + MembershipRequestStatus, + UUID, +} from '@graasp/sdk'; + +import { PartialQueryConfigForApi } from '../../types.js'; +import { + buildDeleteMembershipRequestRoute, + buildGetOwnMembershipRequestRoute, + buildRequestMembershipRoute, +} from './routes.js'; + +export const requestMembership = async ( + { id }: { id: UUID }, + { API_HOST, axios }: PartialQueryConfigForApi, +) => + axios + .post(`${API_HOST}/${buildRequestMembershipRoute(id)}`) + .then(({ data }) => data); + +export const getOwnMembershipRequest = async ( + { id }: { id: UUID }, + { API_HOST, axios }: PartialQueryConfigForApi, +) => + axios + .get<{ + status: MembershipRequestStatus; + }>(`${API_HOST}/${buildGetOwnMembershipRequestRoute(id)}`) + .then(({ data }) => data); + +export const getMembershipRequests = async ( + { id }: { id: UUID }, + { API_HOST, axios }: PartialQueryConfigForApi, +) => + axios + .get< + CompleteMembershipRequest[] + >(`${API_HOST}/${buildRequestMembershipRoute(id)}`) + .then(({ data }) => data); + +export const deleteMembershipRequest = async ( + { itemId, memberId }: { itemId: UUID; memberId: Member['id'] }, + { API_HOST, axios }: PartialQueryConfigForApi, +) => + axios + .delete( + `${API_HOST}/${buildDeleteMembershipRequestRoute({ itemId, memberId })}`, + ) + .then(({ data }) => data); diff --git a/src/membership/request/hooks.test.ts b/src/membership/request/hooks.test.ts new file mode 100644 index 00000000..e220a3d3 --- /dev/null +++ b/src/membership/request/hooks.test.ts @@ -0,0 +1,90 @@ +import { + CompleteMembershipRequest, + MemberFactory, + MembershipRequestStatus, + PackedFolderItemFactory, +} from '@graasp/sdk'; + +import nock from 'nock'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { mockHook, setUpTest } from '../../../test/utils.js'; +import { membershipRequestsKeys } from './keys.js'; +import { + buildGetOwnMembershipRequestRoute, + buildRequestMembershipRoute, +} from './routes.js'; + +const { hooks, wrapper, queryClient } = setUpTest(); + +const item = PackedFolderItemFactory(); +const itemId = item.id; + +describe('Action Hooks', () => { + afterEach(() => { + nock.cleanAll(); + queryClient.clear(); + }); + + describe('useOwnMembershipRequest', () => { + const hook = () => hooks.useOwnMembershipRequest(itemId); + const route = `/${buildGetOwnMembershipRequestRoute(itemId)}`; + + it(`Return own membership request`, async () => { + const response = { + status: MembershipRequestStatus.Approved, + }; + const endpoints = [ + { + route, + response, + }, + ]; + const { data } = await mockHook({ + endpoints, + hook, + wrapper, + }); + + expect(data).toMatchObject(response); + + // verify cache keys + expect( + queryClient.getQueryData(membershipRequestsKeys.own(itemId)), + ).toMatchObject(response); + }); + }); + + describe('useMembershipRequests', () => { + const hook = () => hooks.useMembershipRequests(itemId); + const route = `/${buildRequestMembershipRoute(itemId)}`; + + it(`Return own membership request`, async () => { + const response: CompleteMembershipRequest[] = [ + { + item, + createdAt: new Date().toISOString(), + member: MemberFactory(), + }, + ]; + const endpoints = [ + { + route, + response, + }, + ]; + const { data } = await mockHook({ + endpoints, + hook, + wrapper, + }); + + expect(data).toMatchObject(response); + + // verify cache keys + expect( + queryClient.getQueryData(membershipRequestsKeys.single(itemId)), + ).toMatchObject(response); + }); + }); +}); diff --git a/src/membership/request/hooks.ts b/src/membership/request/hooks.ts new file mode 100644 index 00000000..70761799 --- /dev/null +++ b/src/membership/request/hooks.ts @@ -0,0 +1,34 @@ +import { UUID } from '@graasp/sdk'; + +import { useQuery } from '@tanstack/react-query'; + +import { UndefinedArgument } from '../../config/errors.js'; +import { QueryClientConfig } from '../../types.js'; +import { getMembershipRequests, getOwnMembershipRequest } from './api.js'; +import { membershipRequestsKeys } from './keys.js'; + +export default (queryConfig: QueryClientConfig) => { + const { defaultQueryOptions } = queryConfig; + + return { + useOwnMembershipRequest: (itemId: UUID) => + useQuery({ + queryKey: membershipRequestsKeys.own(itemId), + queryFn: () => getOwnMembershipRequest({ id: itemId }, queryConfig), + ...defaultQueryOptions, + }), + + useMembershipRequests: (id?: UUID, options: { enabled?: boolean } = {}) => + useQuery({ + queryKey: membershipRequestsKeys.single(id), + queryFn: () => { + if (!id) { + throw new UndefinedArgument(); + } + return getMembershipRequests({ id }, queryConfig); + }, + enabled: (options.enabled ?? true) && Boolean(id), + ...defaultQueryOptions, + }), + }; +}; diff --git a/src/membership/request/keys.ts b/src/membership/request/keys.ts new file mode 100644 index 00000000..2ae31f63 --- /dev/null +++ b/src/membership/request/keys.ts @@ -0,0 +1,12 @@ +import { UUID } from '@graasp/sdk'; + +/** + * Contexts + */ +const MEMBERSHIP_REQUESTS_CONTEXT = 'membership-requests'; + +export const membershipRequestsKeys = { + // keys for a single item + single: (itemId?: UUID) => [MEMBERSHIP_REQUESTS_CONTEXT, itemId] as const, + own: (itemId?: UUID) => [MEMBERSHIP_REQUESTS_CONTEXT, itemId, 'own'] as const, +}; diff --git a/src/membership/request/mutations.test.ts b/src/membership/request/mutations.test.ts new file mode 100644 index 00000000..0b4a7624 --- /dev/null +++ b/src/membership/request/mutations.test.ts @@ -0,0 +1,208 @@ +import { + FolderItemFactory, + HttpMethod, + MemberFactory, + MembershipRequestStatus, +} from '@graasp/sdk'; + +import { act } from '@testing-library/react'; +import { StatusCodes } from 'http-status-codes'; +import nock from 'nock'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { OK_RESPONSE, UNAUTHORIZED_RESPONSE } from '../../../test/constants.js'; +import { + mockMutation, + setUpTest, + waitForMutation, +} from '../../../test/utils.js'; +import { membershipRequestsKeys } from './keys.js'; +import { + buildDeleteMembershipRequestRoute, + buildRequestMembershipRoute, +} from './routes.js'; +import { + deleteMembershipRequestRoutine, + requestMembershipRoutine, +} from './routines.js'; + +describe('Membership Request Mutations', () => { + const itemId = FolderItemFactory().id; + + const mockedNotifier = vi.fn(); + const { wrapper, queryClient, mutations } = setUpTest({ + notifier: mockedNotifier, + }); + + afterEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + describe('useRequestMembership', () => { + const route = `/${buildRequestMembershipRoute(itemId)}`; + const mutation = mutations.useRequestMembership; + + it(`Request Membership`, async () => { + queryClient.setQueryData(membershipRequestsKeys.single(itemId), []); + queryClient.setQueryData(membershipRequestsKeys.own(itemId), { + status: MembershipRequestStatus.Approved, + }); + + const endpoints = [ + { route, response: OK_RESPONSE, method: HttpMethod.Post }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ id: itemId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith( + expect.objectContaining({ + type: requestMembershipRoutine.SUCCESS, + }), + ); + + expect( + queryClient.getQueryState(membershipRequestsKeys.single(itemId))! + .isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(membershipRequestsKeys.own(itemId))! + .isInvalidated, + ).toBe(true); + }); + + it(`Unauthorized`, async () => { + queryClient.setQueryData(membershipRequestsKeys.single(itemId), []); + queryClient.setQueryData(membershipRequestsKeys.own(itemId), { + status: MembershipRequestStatus.Approved, + }); + + const endpoints = [ + { + route, + response: UNAUTHORIZED_RESPONSE, + method: HttpMethod.Post, + statusCode: StatusCodes.UNAUTHORIZED, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ id: itemId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith( + expect.objectContaining({ + type: requestMembershipRoutine.FAILURE, + }), + ); + expect( + queryClient.getQueryState(membershipRequestsKeys.single(itemId))! + .isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(membershipRequestsKeys.own(itemId))! + .isInvalidated, + ).toBe(true); + }); + }); + + describe('useDeleteMembershipRequest', () => { + const { id: memberId } = MemberFactory(); + const route = `/${buildDeleteMembershipRequestRoute({ itemId, memberId })}`; + const mutation = mutations.useDeleteMembershipRequest; + + it(`Delete Request Membership`, async () => { + queryClient.setQueryData(membershipRequestsKeys.single(itemId), []); + queryClient.setQueryData(membershipRequestsKeys.own(itemId), { + status: MembershipRequestStatus.Approved, + }); + + const endpoints = [ + { route, response: OK_RESPONSE, method: HttpMethod.Delete }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ itemId, memberId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith( + expect.objectContaining({ + type: deleteMembershipRequestRoutine.SUCCESS, + }), + ); + + expect( + queryClient.getQueryState(membershipRequestsKeys.single(itemId))! + .isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(membershipRequestsKeys.own(itemId))! + .isInvalidated, + ).toBe(true); + }); + + it(`Unauthorized`, async () => { + queryClient.setQueryData(membershipRequestsKeys.single(itemId), []); + queryClient.setQueryData(membershipRequestsKeys.own(itemId), { + status: MembershipRequestStatus.Approved, + }); + + const endpoints = [ + { + route, + response: UNAUTHORIZED_RESPONSE, + method: HttpMethod.Delete, + statusCode: StatusCodes.UNAUTHORIZED, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + mockedMutation.mutate({ itemId, memberId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith( + expect.objectContaining({ + type: deleteMembershipRequestRoutine.FAILURE, + }), + ); + expect( + queryClient.getQueryState(membershipRequestsKeys.single(itemId))! + .isInvalidated, + ).toBe(true); + expect( + queryClient.getQueryState(membershipRequestsKeys.own(itemId))! + .isInvalidated, + ).toBe(true); + }); + }); +}); diff --git a/src/membership/request/mutations.ts b/src/membership/request/mutations.ts new file mode 100644 index 00000000..2bf6d872 --- /dev/null +++ b/src/membership/request/mutations.ts @@ -0,0 +1,65 @@ +import { Member, UUID } from '@graasp/sdk'; +import { SUCCESS_MESSAGES } from '@graasp/translations'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { QueryClientConfig } from '../../types.js'; +import { deleteMembershipRequest, requestMembership } from './api.js'; +import { membershipRequestsKeys } from './keys.js'; +import { + deleteMembershipRequestRoutine, + requestMembershipRoutine, +} from './routines.js'; + +export default (queryConfig: QueryClientConfig) => { + const { notifier } = queryConfig; + + const useRequestMembership = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: { id: UUID }) => requestMembership(payload, queryConfig), + { + onSuccess: () => { + notifier?.({ + type: requestMembershipRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.REQUEST_MEMBERSHIP }, + }); + }, + onError: (error: Error, _args, _context) => { + notifier?.({ + type: requestMembershipRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { id }) => { + queryClient.invalidateQueries(membershipRequestsKeys.single(id)); + }, + }, + ); + }; + const useDeleteMembershipRequest = () => { + const queryClient = useQueryClient(); + return useMutation( + (payload: { itemId: UUID; memberId: Member['id'] }) => + deleteMembershipRequest(payload, queryConfig), + { + onSuccess: () => { + notifier?.({ + type: deleteMembershipRequestRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.DELETE_MEMBERSHIP_REQUEST }, + }); + }, + onError: (error: Error, _args, _context) => { + notifier?.({ + type: deleteMembershipRequestRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { itemId }) => { + queryClient.invalidateQueries(membershipRequestsKeys.single(itemId)); + }, + }, + ); + }; + return { useRequestMembership, useDeleteMembershipRequest }; +}; diff --git a/src/membership/request/routes.ts b/src/membership/request/routes.ts new file mode 100644 index 00000000..7f138efc --- /dev/null +++ b/src/membership/request/routes.ts @@ -0,0 +1,17 @@ +import { Member, UUID } from '@graasp/sdk'; + +import { ITEMS_ROUTE } from '../../routes.js'; + +export const buildRequestMembershipRoute = (id: UUID) => + `${ITEMS_ROUTE}/${id}/memberships/requests`; + +export const buildGetMembershipRequestsRoute = (id: UUID) => + `${ITEMS_ROUTE}/${id}/memberships/requests`; + +export const buildGetOwnMembershipRequestRoute = (id: UUID) => + `${ITEMS_ROUTE}/${id}/memberships/requests/own`; + +export const buildDeleteMembershipRequestRoute = (args: { + itemId: UUID; + memberId: Member['id']; +}) => `${ITEMS_ROUTE}/${args.itemId}/memberships/requests/${args.memberId}`; diff --git a/src/membership/request/routines.ts b/src/membership/request/routines.ts new file mode 100644 index 00000000..732df303 --- /dev/null +++ b/src/membership/request/routines.ts @@ -0,0 +1,6 @@ +import createRoutine from '../../routines/utils.js'; + +export const requestMembershipRoutine = createRoutine('REQUEST_MEMBERSHIP'); +export const deleteMembershipRequestRoutine = createRoutine( + 'DELETE_MEMBERSHIP_REQUEST', +); diff --git a/src/mutations/index.ts b/src/mutations/index.ts index a48e647c..34773c39 100644 --- a/src/mutations/index.ts +++ b/src/mutations/index.ts @@ -1,6 +1,7 @@ import itemMutations from '../item/mutations.js'; import memberMutations from '../member/mutations.js'; import publicProfileMutations from '../member/publicProfile/mutations.js'; +import membershipRequestsMutations from '../membership/request/mutations.js'; import { QueryClientConfig } from '../types.js'; import actionMutations from './action.js'; import authenticationMutations from './authentication.js'; @@ -45,6 +46,7 @@ const configureMutations = (queryConfig: QueryClientConfig) => ({ ...publicProfileMutations(queryConfig), ...shortLinksMutations(queryConfig), ...tagsMutations(queryConfig), + ...membershipRequestsMutations(queryConfig), }); export default configureMutations; diff --git a/src/mutations/itemLogin.ts b/src/mutations/itemLogin.ts index 68d3ed04..14124233 100644 --- a/src/mutations/itemLogin.ts +++ b/src/mutations/itemLogin.ts @@ -4,6 +4,7 @@ import { SUCCESS_MESSAGES } from '@graasp/translations'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api/itemLogin.js'; +import { useEnroll } from '../item/itemLogin/mutations.js'; import { itemKeys } from '../keys.js'; import { deleteItemLoginSchemaRoutine, @@ -106,6 +107,7 @@ export default (queryConfig: QueryClientConfig) => { }; return { + useEnroll: useEnroll(queryConfig), usePostItemLogin, useDeleteItemLoginSchema, usePutItemLoginSchema, diff --git a/src/mutations/membership.ts b/src/mutations/membership.ts index d7b13381..9c2783f0 100644 --- a/src/mutations/membership.ts +++ b/src/mutations/membership.ts @@ -12,6 +12,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as InvitationApi from '../api/invitation.js'; import * as Api from '../api/membership.js'; import { buildManyItemMembershipsKey, itemKeys } from '../keys.js'; +import { membershipRequestsKeys } from '../membership/request/keys.js'; import { deleteItemMembershipRoutine, editItemMembershipRoutine, @@ -50,6 +51,9 @@ export default (queryConfig: QueryClientConfig) => { // this won't trigger too many errors as long as the stale time is low queryClient.invalidateQueries(buildManyItemMembershipsKey([id])); queryClient.invalidateQueries(itemKeys.single(id).memberships); + + // membership might come from request, so we invalidate them + queryClient.invalidateQueries(membershipRequestsKeys.single(id)); }, }, ); diff --git a/src/routines/index.ts b/src/routines/index.ts index bd9d7cd3..f2c82a89 100644 --- a/src/routines/index.ts +++ b/src/routines/index.ts @@ -1,6 +1,7 @@ export * from '../item/routines.js'; export * from '../member/publicProfile/routines.js'; export * from '../member/subscription/routines.js'; +export * from '../membership/request/routines.js'; export * from '../member/routines.js'; export * from './action.js'; export * from './authentication.js'; diff --git a/yarn.lock b/yarn.lock index 0eff914a..35b555c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -625,7 +625,7 @@ __metadata: "@eslint/eslintrc": "npm:3.1.0" "@eslint/js": "npm:9.10.0" "@graasp/sdk": "npm:4.29.1" - "@graasp/translations": "npm:1.37.1" + "@graasp/translations": "npm:1.38.0" "@tanstack/react-query": "npm:4.36.1" "@tanstack/react-query-devtools": "npm:4.36.1" "@testing-library/dom": "npm:10.4.0" @@ -686,12 +686,12 @@ __metadata: languageName: node linkType: hard -"@graasp/translations@npm:1.37.1": - version: 1.37.1 - resolution: "@graasp/translations@npm:1.37.1" +"@graasp/translations@npm:1.38.0": + version: 1.38.0 + resolution: "@graasp/translations@npm:1.38.0" peerDependencies: i18next: ^23.8.1 - checksum: 10/9ccc50b2808ba11479b2ebcb231495f952b0d14118526e9c9080f28ba0be4955276b3846c53ab7eb66b82e0df0bdf69f32c0130cc970698d8d952edf954aa1ab + checksum: 10/4ba3ce113c2df86d1fe7a8c379a23ae015d6ada7f5bcafa96b53212e1016af6ba92679235fbaafcb0b074ddf7f535510573a3a7c944af89eb5d46eb9bfe64c08 languageName: node linkType: hard