Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: library profile mutations #514

Merged
merged 14 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './membership';
export * from './mentions';
export * from './search';
export * from './subscription';
export * from './publicProfile';
55 changes: 55 additions & 0 deletions src/api/publicProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { UUID } from '@graasp/sdk';

import { PartialQueryConfigForApi } from '../types';
import {
GET_OWN_PROFILE,
PUBLIC_PROFILE_ROUTE,
buildGetPublicProfileRoute,
} from './routes';

export const getOwnProfile = ({ API_HOST, axios }: PartialQueryConfigForApi) =>
axios.get(`${API_HOST}/${GET_OWN_PROFILE}`).then(({ data }) => data);

export const getPublicProfile = (
memberId: UUID,
{ API_HOST, axios }: PartialQueryConfigForApi,
) =>
axios
.get(`${API_HOST}/${buildGetPublicProfileRoute(memberId)}`)
.then(({ data }) => data);

export type PostProfilePayloadType = {
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
bio: string;
twitterLink?: string;
facebookLink?: string;
linkedinLink?: string;
visibility?: boolean;
};

export const postProfile = async (
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
{
bio,
twitterLink,
facebookLink,
linkedinLink,
visibility = false,
}: PostProfilePayloadType,
{ API_HOST, axios }: PartialQueryConfigForApi,
) =>
axios
.post(`${API_HOST}/${PUBLIC_PROFILE_ROUTE}`, {
bio,
twitterLink,
facebookLink,
linkedinLink,
visibility,
})
.then(({ data }) => data);

export const patchProfile = async (
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
arg: Partial<PostProfilePayloadType>,
{ API_HOST, axios }: PartialQueryConfigForApi,
) =>
axios
.patch(`${API_HOST}/${PUBLIC_PROFILE_ROUTE}`, arg)
.then(({ data }) => data);
8 changes: 8 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ export const buildGetEtherpadRoute = (itemId: UUID) =>

export const SEARCH_PUBLISHED_ITEMS_ROUTE = `${ITEMS_ROUTE}/${COLLECTIONS_ROUTE}/search`;

export const PUBLIC_PROFILE_ROUTE = `profile`;
export const GET_OWN_PROFILE = `${PUBLIC_PROFILE_ROUTE}/own`;
export const buildGetPublicProfileRoute = (memberId: UUID) =>
`${PUBLIC_PROFILE_ROUTE}/${memberId}`;

export const API_ROUTES = {
APPS_ROUTE,
buildAppListRoute,
Expand Down Expand Up @@ -499,4 +504,7 @@ export const API_ROUTES = {
SIGN_OUT_ROUTE,
SIGN_UP_ROUTE,
VALIDATION_ROUTE,
PUBLIC_PROFILE_ROUTE,
GET_OWN_PROFILE,
buildGetPublicProfileRoute,
};
4 changes: 4 additions & 0 deletions src/config/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export const buildSearchPublishedItemsKey = (args: {

export const CURRENT_MEMBER_STORAGE_KEY = [MEMBERS_KEY, 'current', 'storage'];

export const OWN_LIBRARY_PROFILE_KEY = ['own-profile'];
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
export const buildPublicProfileKey = (memberId: UUID) => ['profile', memberId];
export const DATA_KEYS = {
APPS_KEY,
ITEMS_KEY,
Expand Down Expand Up @@ -295,4 +297,6 @@ export const DATA_KEYS = {
buildPublishedItemsKey,
buildEtherpadKey,
buildSearchPublishedItemsKey,
OWN_LIBRARY_PROFILE_KEY,
buildPublicProfileKey,
};
2 changes: 2 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import configureMemberHooks from './member';
import configureMembershipHooks from './membership';
import configureMentionsHooks from './mention';
import configurePlanHooks from './plan';
import configurePublicProfileHooks from './publicProfile';
import configureKeywordSearchHooks from './search';

export default (
Expand Down Expand Up @@ -51,5 +52,6 @@ export default (
...configureInvitationHooks(queryConfig),
...memberHooks,
...configurePlanHooks(queryConfig),
...configurePublicProfileHooks(queryConfig),
};
};
77 changes: 77 additions & 0 deletions src/hooks/publicProfile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { StatusCodes } from 'http-status-codes';
import nock from 'nock';

import {
MEMBER_PUBLIC_PROFILE,
UNAUTHORIZED_RESPONSE,
} from '../../test/constants';
import { mockHook, setUpTest } from '../../test/utils';
import { GET_OWN_PROFILE, buildGetPublicProfileRoute } from '../api/routes';
import { OWN_LIBRARY_PROFILE_KEY, buildPublicProfileKey } from '../config/keys';

const { hooks, wrapper, queryClient } = setUpTest();

describe('Public Profile Hooks', () => {
afterEach(() => {
nock.cleanAll();
queryClient.clear();
});

describe('useOwnProfile', () => {
const route = `/${GET_OWN_PROFILE}`;

const response = MEMBER_PUBLIC_PROFILE;

const hook = () => hooks.useOwnProfile();
it(`Receive actions for item id`, async () => {
const endpoints = [{ route, response }];
const { data } = await mockHook({ endpoints, hook, wrapper });
expect(data).toEqual(response);
expect(queryClient.getQueryData(OWN_LIBRARY_PROFILE_KEY)).toEqual(
response,
);
});

it(`Unauthorized`, async () => {
const endpoints = [
{
route,
response: UNAUTHORIZED_RESPONSE,
statusCode: StatusCodes.UNAUTHORIZED,
},
];
const { isError } = await mockHook({
endpoints,
hook,
wrapper,
});

expect(isError).toBeTruthy();
expect(queryClient.getQueryData(OWN_LIBRARY_PROFILE_KEY)).toBeFalsy();
});
});

describe('usePublicProfile', () => {
const id = 'member-id';
const response = MEMBER_PUBLIC_PROFILE;

it(`Receive member public profile for member-id = ${id}`, async () => {
const endpoints = [
{
route: `/${buildGetPublicProfileRoute(id)}`,
response,
},
];
const hook = () => hooks.usePublicProfile(id);
const { data } = await mockHook({
hook,
wrapper,
endpoints,
});
expect(queryClient.getQueryData(buildPublicProfileKey(id))).toEqual(
response,
);
expect(data).toMatchObject(response);
});
});
});
34 changes: 34 additions & 0 deletions src/hooks/publicProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UUID } from '@graasp/sdk';

import { useQuery } from 'react-query';

import * as Api from '../api';
import { UndefinedArgument } from '../config/errors';
import { OWN_LIBRARY_PROFILE_KEY, buildPublicProfileKey } from '../config/keys';
import { QueryClientConfig } from '../types';

export default (queryConfig: QueryClientConfig) => {
const { defaultQueryOptions } = queryConfig;

return {
useOwnProfile: () =>
useQuery({
queryKey: OWN_LIBRARY_PROFILE_KEY,
queryFn: () => Api.getOwnProfile(queryConfig),
...defaultQueryOptions,
}),

usePublicProfile: (memberId: UUID) =>
useQuery({
queryKey: buildPublicProfileKey(memberId),
queryFn: () => {
if (!memberId) {
throw new UndefinedArgument();
}
return Api.getPublicProfile(memberId, queryConfig);
},
enabled: Boolean(memberId),
...defaultQueryOptions,
}),
};
};
2 changes: 2 additions & 0 deletions src/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import memberMutations from './member';
import itemMembershipMutations from './membership';
import mentionMutations from './mention';
import subscriptionMutations from './plan';
import publicProfileMutations from './publicProfile';

const configureMutations = (queryConfig: QueryClientConfig) => ({
...itemMutations(queryConfig),
Expand All @@ -37,6 +38,7 @@ const configureMutations = (queryConfig: QueryClientConfig) => ({
...authenticationMutations(queryConfig),
...subscriptionMutations(queryConfig),
...itemPublishMutations(queryConfig),
...publicProfileMutations(queryConfig),
});

export default configureMutations;
102 changes: 102 additions & 0 deletions src/mutations/publicProfile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { HttpMethod } from '@graasp/sdk';

import { act } from '@testing-library/react';
import nock from 'nock';

import { MEMBER_RESPONSE } from '../../test/constants';
import { mockMutation, setUpTest, waitForMutation } from '../../test/utils';
import { PUBLIC_PROFILE_ROUTE } from '../api/routes';
import { OWN_LIBRARY_PROFILE_KEY } from '../config/keys';

const mockedNotifier = jest.fn();
const { wrapper, queryClient, mutations } = setUpTest({
notifier: mockedNotifier,
});
const newProfile = {
bio: 'text',
twitterLink: 'https://twitter.com/test',
facebookLink: 'https://facebook.com/test',
linkedinLink: 'https://linkedin.com/test',
visibility: false,
};
describe('Public Profile Mutations', () => {
afterEach(() => {
queryClient.clear();
nock.cleanAll();
mockedNotifier.mockClear();
});

describe('usePostProfile', () => {
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
const mutation = mutations.usePostProfile;

it('Post profile', async () => {
const route = `/${PUBLIC_PROFILE_ROUTE}`;
const response = { ...newProfile, id: 'someid', member: MEMBER_RESPONSE };

queryClient.setQueryData(OWN_LIBRARY_PROFILE_KEY, response);

const endpoints = [
{
response,
method: HttpMethod.POST,
route,
},
];

const mockedMutation = await mockMutation({
endpoints,
mutation,
wrapper,
});

await act(async () => {
await mockedMutation.mutate(newProfile);
await waitForMutation();
});

expect(queryClient.getQueryState(OWN_LIBRARY_PROFILE_KEY)?.data).toEqual(
response,
);
});
});

describe('usePatchProfile', () => {
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
const mutation = mutations.usePatchProfile;
const payload = { bio: 'new description' };
const result = {
...newProfile,
bio: 'new description',
id: 'someid',
member: MEMBER_RESPONSE,
};

it('Edit item in root', async () => {
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
const route = `/${PUBLIC_PROFILE_ROUTE}`;
const response = null;
const endpoints = [
{
response,
method: HttpMethod.PATCH,
route,
},
];
queryClient.setQueryData(OWN_LIBRARY_PROFILE_KEY, result);

const mockedMutation = await mockMutation({
endpoints,
mutation,
wrapper,
});

await act(async () => {
await mockedMutation.mutate(payload);
await waitForMutation();
});

expect(queryClient.getQueryState(OWN_LIBRARY_PROFILE_KEY)?.data).toEqual(
result,
);
expect(queryClient.getQueryData(OWN_LIBRARY_PROFILE_KEY)).toEqual(result);
});
});
});
48 changes: 48 additions & 0 deletions src/mutations/publicProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMutation } from 'react-query';

import * as Api from '../api';
import { PostProfilePayloadType } from '../api';
import { patchProfileRoutine, postProfileRoutine } from '../routines';
import type { QueryClientConfig } from '../types';

export default (queryConfig: QueryClientConfig) => {
const { notifier } = queryConfig;

const usePostProfile = () =>
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
useMutation(
async (profileData: PostProfilePayloadType) =>
Api.postProfile(profileData, queryConfig),
{
onSuccess: () => {
notifier?.({
type: postProfileRoutine.SUCCESS,
payload: { message: 'Your Data Saved Successfully' },
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
});
},
onError: (error: Error) => {
notifier?.({ type: postProfileRoutine.FAILURE, payload: { error } });
},
},
);
const usePatchProfile = () =>
useMutation(
(payload: Partial<PostProfilePayloadType>) =>
Api.patchProfile(payload, queryConfig),
{
onSuccess: () => {
notifier?.({
type: patchProfileRoutine.SUCCESS,
payload: { message: 'Your Data Saved Successfully' },
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
});
},
onError: (error: Error) => {
notifier?.({ type: patchProfileRoutine.FAILURE, payload: { error } });
},
},
);

return {
usePostProfile,
usePatchProfile,
};
};
1 change: 1 addition & 0 deletions src/routines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './invitation';
export * from './authentication';
export * from './plan';
export * from './itemPublish';
export * from './libraryProfile';
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
Loading