Add ability to remove profile picture on Profile Settings (#538)

* Add ability to remove profile picture on Profile Settings

* Fix lint

* Fix according to review
This commit is contained in:
Charles Bochet
2023-07-08 10:41:16 -07:00
committed by GitHub
parent e2822ed095
commit 36ace6cc03
22 changed files with 363 additions and 75 deletions

View File

@ -3037,13 +3037,6 @@ export type SearchCompanyQueryVariables = Exact<{
export type SearchCompanyQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Company', id: string, name: string, domainName: string }> };
export type UploadProfilePictureMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: string };
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@ -3062,6 +3055,20 @@ export type UpdateUserMutationVariables = Exact<{
export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } };
export type UploadProfilePictureMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: string };
export type RemoveProfilePictureMutationVariables = Exact<{
where: UserWhereUniqueInput;
}>;
export type RemoveProfilePictureMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string } };
export type GetCurrentWorkspaceQueryVariables = Exact<{ [key: string]: never; }>;
@ -4358,37 +4365,6 @@ export function useSearchCompanyLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
export type SearchCompanyQueryHookResult = ReturnType<typeof useSearchCompanyQuery>;
export type SearchCompanyLazyQueryHookResult = ReturnType<typeof useSearchCompanyLazyQuery>;
export type SearchCompanyQueryResult = Apollo.QueryResult<SearchCompanyQuery, SearchCompanyQueryVariables>;
export const UploadProfilePictureDocument = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file)
}
`;
export type UploadProfilePictureMutationFn = Apollo.MutationFunction<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
/**
* __useUploadProfilePictureMutation__
*
* To run a mutation, you first call `useUploadProfilePictureMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadProfilePictureMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadProfilePictureMutation, { data, loading, error }] = useUploadProfilePictureMutation({
* variables: {
* file: // value for 'file'
* },
* });
*/
export function useUploadProfilePictureMutation(baseOptions?: Apollo.MutationHookOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>(UploadProfilePictureDocument, options);
}
export type UploadProfilePictureMutationHookResult = ReturnType<typeof useUploadProfilePictureMutation>;
export type UploadProfilePictureMutationResult = Apollo.MutationResult<UploadProfilePictureMutation>;
export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
export const GetCurrentUserDocument = gql`
query GetCurrentUser {
currentUser {
@ -4514,6 +4490,70 @@ export function useUpdateUserMutation(baseOptions?: Apollo.MutationHookOptions<U
export type UpdateUserMutationHookResult = ReturnType<typeof useUpdateUserMutation>;
export type UpdateUserMutationResult = Apollo.MutationResult<UpdateUserMutation>;
export type UpdateUserMutationOptions = Apollo.BaseMutationOptions<UpdateUserMutation, UpdateUserMutationVariables>;
export const UploadProfilePictureDocument = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file)
}
`;
export type UploadProfilePictureMutationFn = Apollo.MutationFunction<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
/**
* __useUploadProfilePictureMutation__
*
* To run a mutation, you first call `useUploadProfilePictureMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadProfilePictureMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadProfilePictureMutation, { data, loading, error }] = useUploadProfilePictureMutation({
* variables: {
* file: // value for 'file'
* },
* });
*/
export function useUploadProfilePictureMutation(baseOptions?: Apollo.MutationHookOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>(UploadProfilePictureDocument, options);
}
export type UploadProfilePictureMutationHookResult = ReturnType<typeof useUploadProfilePictureMutation>;
export type UploadProfilePictureMutationResult = Apollo.MutationResult<UploadProfilePictureMutation>;
export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
export const RemoveProfilePictureDocument = gql`
mutation RemoveProfilePicture($where: UserWhereUniqueInput!) {
updateUser(data: {avatarUrl: {set: null}}, where: $where) {
id
}
}
`;
export type RemoveProfilePictureMutationFn = Apollo.MutationFunction<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>;
/**
* __useRemoveProfilePictureMutation__
*
* To run a mutation, you first call `useRemoveProfilePictureMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveProfilePictureMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeProfilePictureMutation, { data, loading, error }] = useRemoveProfilePictureMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useRemoveProfilePictureMutation(baseOptions?: Apollo.MutationHookOptions<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>(RemoveProfilePictureDocument, options);
}
export type RemoveProfilePictureMutationHookResult = ReturnType<typeof useRemoveProfilePictureMutation>;
export type RemoveProfilePictureMutationResult = Apollo.MutationResult<RemoveProfilePictureMutation>;
export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>;
export const GetCurrentWorkspaceDocument = gql`
query GetCurrentWorkspace {
currentWorkspace {

View File

@ -6,7 +6,7 @@ import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { GET_CURRENT_USER } from '@/users/services';
import { GET_CURRENT_USER } from '@/users/queries';
import { useUpdateUserMutation } from '~/generated/graphql';
const StyledComboInputContainer = styled.div`

View File

@ -3,13 +3,21 @@ import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { GET_CURRENT_USER } from '@/users/services';
import { useUploadProfilePictureMutation } from '~/generated/graphql';
import { GET_CURRENT_USER } from '@/users/queries';
import { getImageAbsoluteURI } from '@/users/utils/getProfilePictureAbsoluteURI';
import {
useRemoveProfilePictureMutation,
useUploadProfilePictureMutation,
} from '~/generated/graphql';
export function PictureUploader() {
const [uploadPicture] = useUploadProfilePictureMutation();
const [removePicture] = useRemoveProfilePictureMutation();
const [currentUser] = useRecoilState(currentUserState);
async function onUpload(file: File) {
if (!file) {
return;
}
await uploadPicture({
variables: {
file,
@ -18,8 +26,22 @@ export function PictureUploader() {
});
}
const pictureUrl = currentUser?.avatarUrl
? `${process.env.REACT_APP_FILES_URL}/${currentUser?.avatarUrl}`
: null;
return <ImageInput picture={pictureUrl} onUpload={onUpload} />;
async function onRemove() {
await removePicture({
variables: {
where: {
id: currentUser?.id,
},
},
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
});
}
return (
<ImageInput
picture={getImageAbsoluteURI(currentUser?.avatarUrl)}
onUpload={onUpload}
onRemove={onRemove}
/>
);
}

View File

@ -1,7 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_PROFILE_PICTURE = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file)
}
`;

View File

@ -23,6 +23,7 @@ const Picture = styled.button<{ withPicture: boolean }>`
height: 66px;
justify-content: center;
overflow: hidden;
padding: 0;
transition: background 0.1s ease;
width: 66px;
@ -132,7 +133,7 @@ export function ImageInput({
onClick={onRemove}
variant="secondary"
title="Remove"
disabled
disabled={!picture || disabled}
fullWidth
/>
</ButtonContainer>

View File

@ -12,6 +12,7 @@ type OwnProps = {
};
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
align-items: center;
background-color: ${(props) =>
!isNonEmptyString(props.avatarUrl)
? props.theme.background.tertiary
@ -22,12 +23,12 @@ export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-shrink: 0;
flex-shrink: 0;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${(props) => props.size}px;
height: ${(props) => props.size}px;
justify-content: center;
width: ${(props) => props.size}px;
`;

View File

@ -0,0 +1,28 @@
import { gql } from '@apollo/client';
export const UPDATE_USER = gql`
mutation UpdateUser($data: UserUpdateInput!, $where: UserWhereUniqueInput!) {
updateUser(data: $data, where: $where) {
id
email
displayName
firstName
lastName
avatarUrl
}
}
`;
export const UPDATE_PROFILE_PICTURE = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file)
}
`;
export const REMOVE_PROFILE_PICTURE = gql`
mutation RemoveProfilePicture($where: UserWhereUniqueInput!) {
updateUser(data: { avatarUrl: { set: null } }, where: $where) {
id
}
}
`;

View File

@ -1,14 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_USER = gql`
mutation UpdateUser($data: UserUpdateInput!, $where: UserWhereUniqueInput!) {
updateUser(data: $data, where: $where) {
id
email
displayName
firstName
lastName
avatarUrl
}
}
`;

View File

@ -0,0 +1,5 @@
export function getImageAbsoluteURI(imageRelativePath?: string | null) {
return imageRelativePath
? `${process.env.REACT_APP_FILES_URL}/${imageRelativePath}`
: null;
}

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { Avatar } from '@/users/components/Avatar';
import { getImageAbsoluteURI } from '@/users/utils/getProfilePictureAbsoluteURI';
import { User } from '~/generated/graphql';
const StyledContainer = styled.div`
@ -47,7 +48,7 @@ export function WorkspaceMemberCard({ workspaceMember }: OwnProps) {
<StyledContainer>
<AvatarContainer>
<Avatar
avatarUrl={workspaceMember.user.avatarUrl}
avatarUrl={getImageAbsoluteURI(workspaceMember.user.avatarUrl)}
placeholder={workspaceMember.user.firstName || ''}
type="squared"
size={40}

View File

@ -16,7 +16,7 @@ import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
import { GET_CURRENT_USER } from '@/users/services';
import { GET_CURRENT_USER } from '@/users/queries';
import { useUpdateUserMutation } from '~/generated/graphql';
const StyledContentContainer = styled.div`

View File

@ -13,7 +13,7 @@ import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
import { GET_CURRENT_USER } from '@/users/services';
import { GET_CURRENT_USER } from '@/users/queries';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
const StyledContentContainer = styled.div`

View File

@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AuthModal } from '@/auth/components/ui/Modal';
import { AuthLayout } from '@/ui/layout/AuthLayout';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { CreateProfile } from '../CreateProfile';
const meta: Meta<typeof CreateProfile> = {
title: 'Pages/Auth/CreateProfile',
component: CreateProfile,
};
export default meta;
export type Story = StoryObj<typeof CreateProfile>;
export const Default: Story = {
render: getRenderWrapperForPage(
<AuthLayout>
<AuthModal>
<CreateProfile />
</AuthModal>
</AuthLayout>,
'/auth/create-profile',
),
parameters: {
msw: graphqlMocks,
},
};

View File

@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AuthModal } from '@/auth/components/ui/Modal';
import { AuthLayout } from '@/ui/layout/AuthLayout';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { CreateWorkspace } from '../CreateWorkspace';
const meta: Meta<typeof CreateWorkspace> = {
title: 'Pages/Auth/CreateWorkspace',
component: CreateWorkspace,
};
export default meta;
export type Story = StoryObj<typeof CreateWorkspace>;
export const Default: Story = {
render: getRenderWrapperForPage(
<AuthLayout>
<AuthModal>
<CreateWorkspace />
</AuthModal>
</AuthLayout>,
'/auth/create-workspace',
),
parameters: {
msw: graphqlMocks,
},
};

View File

@ -0,0 +1,11 @@
{ /* Opportunities.mdx */ }
import { Canvas, Meta } from '@storybook/blocks';
import * as Opportunities from './Opportunities.stories';
<Meta of={Opportunities} />
# Opportunities View
<Canvas of={Opportunities.Default} />

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { Opportunities } from '../Opportunities';
const meta: Meta<typeof Opportunities> = {
title: 'Pages/Opportunities',
component: Opportunities,
};
export default meta;
export type Story = StoryObj<typeof Opportunities>;
export const Default: Story = {
render: getRenderWrapperForPage(<Opportunities />, '/opportunities'),
parameters: {
msw: graphqlMocks,
},
};

View File

@ -77,7 +77,7 @@ export const CompanyName: Story = {
delay: 200,
});
await sleep(1000);
await sleep(500);
const qontoChip = canvas
.getAllByTestId('dropdown-menu-item')

View File

@ -197,12 +197,14 @@ export const EditRelation: Story = {
let secondRowCompanyCell = await canvas.findByText(
mockedPeopleData[1].company.name,
);
await sleep(25);
await userEvent.click(secondRowCompanyCell);
secondRowCompanyCell = await canvas.findByText(
mockedPeopleData[1].company.name,
);
await sleep(25);
await userEvent.click(secondRowCompanyCell);
@ -240,11 +242,13 @@ export const SelectRelationWithKeys: Story = {
let firstRowCompanyCell = await canvas.findByText(
mockedPeopleData[0].company.name,
);
await sleep(25);
await userEvent.click(firstRowCompanyCell);
firstRowCompanyCell = await canvas.findByText(
mockedPeopleData[0].company.name,
);
await sleep(25);
await userEvent.click(firstRowCompanyCell);
const relationInput = await canvas.findByPlaceholderText('Search');

View File

@ -2,13 +2,15 @@ import { getOperationName } from '@apollo/client/utilities';
import { graphql } from 'msw';
import { CREATE_EVENT } from '@/analytics/services';
import { GET_CLIENT_CONFIG } from '@/client-config/queries';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE, UPDATE_PERSON } from '@/people/services';
import { GET_PIPELINES } from '@/pipeline-progress/queries';
import {
SEARCH_COMPANY_QUERY,
SEARCH_USER_QUERY,
} from '@/search/services/search';
import { GET_CURRENT_USER } from '@/users/services';
import { GET_CURRENT_USER } from '@/users/queries';
import {
GetCompaniesQuery,
GetPeopleQuery,
@ -18,6 +20,7 @@ import {
import { mockedCompaniesData } from './mock-data/companies';
import { mockedPeopleData } from './mock-data/people';
import { mockedPipelinesData } from './mock-data/pipelines';
import { mockedUsersData } from './mock-data/users';
import { filterAndSortData, updateOneFromData } from './mock-data';
@ -103,6 +106,13 @@ export const graphqlMocks = [
}),
);
}),
graphql.query(getOperationName(GET_PIPELINES) ?? '', (req, res, ctx) => {
return res(
ctx.data({
findManyPipeline: mockedPipelinesData,
}),
);
}),
graphql.mutation(getOperationName(CREATE_EVENT) ?? '', (req, res, ctx) => {
return res(
ctx.data({
@ -110,4 +120,16 @@ export const graphqlMocks = [
}),
);
}),
graphql.query(getOperationName(GET_CLIENT_CONFIG) ?? '', (req, res, ctx) => {
return res(
ctx.data({
clientConfig: {
demoMode: true,
debugMode: false,
authProviders: { google: true, password: true, magicLink: false },
telemetry: { enabled: false, anonymizationEnabled: true },
},
}),
);
}),
];

View File

@ -0,0 +1,90 @@
import {
Pipeline,
PipelineProgress,
PipelineProgressableType,
PipelineStage,
} from '../../generated/graphql';
type MockedPipeline = Pick<
Pipeline,
'id' | 'name' | 'pipelineProgressableType' | '__typename'
> & {
pipelineStages: Array<
Pick<PipelineStage, 'id' | 'name' | 'color' | '__typename'> & {
pipelineProgresses: Array<
Pick<
PipelineProgress,
| 'id'
| 'progressableType'
| 'progressableId'
| 'amount'
| 'closeDate'
| '__typename'
>
>;
}
>;
};
export const mockedPipelinesData: Array<MockedPipeline> = [
{
id: 'fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
name: 'Sales pipeline',
pipelineProgressableType: PipelineProgressableType.Company,
pipelineStages: [
{
id: 'fe256b39-3ec3-4fe3-8998-b76aa0bfb600',
name: 'New',
color: '#B76796',
pipelineProgresses: [
{
id: 'fe256b39-3ec3-4fe7-8998-b76aa0bfb600',
progressableType: PipelineProgressableType.Company,
progressableId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
amount: null,
closeDate: null,
__typename: 'PipelineProgress',
},
{
id: '4a886c90-f4f2-4984-8222-882ebbb905d6',
progressableType: PipelineProgressableType.Company,
progressableId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
amount: null,
closeDate: null,
__typename: 'PipelineProgress',
},
],
__typename: 'PipelineStage',
},
{
id: 'fe256b39-3ec3-4fe4-8998-b76aa0bfb600',
name: 'Screening',
color: '#CB912F',
pipelineProgresses: [],
__typename: 'PipelineStage',
},
{
id: 'fe256b39-3ec3-4fe5-8998-b76aa0bfb600',
name: 'Meeting',
color: '#9065B0',
pipelineProgresses: [],
__typename: 'PipelineStage',
},
{
id: 'fe256b39-3ec3-4fe6-8998-b76aa0bfb600',
name: 'Proposal',
color: '#337EA9',
pipelineProgresses: [],
__typename: 'PipelineStage',
},
{
id: 'fe256b39-3ec3-4fe7-8998-b76aa0bfb600',
name: 'Customer',
color: '#079039',
pipelineProgresses: [],
__typename: 'PipelineStage',
},
],
__typename: 'Pipeline',
},
];

View File

@ -1,4 +1,4 @@
import { Args, Mutation, Resolver, Query } from '@nestjs/graphql';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthTokens } from './dto/token.entity';
import { TokenService } from './services/token.service';
import { RefreshTokenInput } from './dto/refresh-token.input';