diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 630126b32..11d23a30b 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -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; export type SearchCompanyLazyQueryHookResult = ReturnType; export type SearchCompanyQueryResult = Apollo.QueryResult; -export const UploadProfilePictureDocument = gql` - mutation UploadProfilePicture($file: Upload!) { - uploadProfilePicture(file: $file) -} - `; -export type UploadProfilePictureMutationFn = Apollo.MutationFunction; - -/** - * __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) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UploadProfilePictureDocument, options); - } -export type UploadProfilePictureMutationHookResult = ReturnType; -export type UploadProfilePictureMutationResult = Apollo.MutationResult; -export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions; export const GetCurrentUserDocument = gql` query GetCurrentUser { currentUser { @@ -4514,6 +4490,70 @@ export function useUpdateUserMutation(baseOptions?: Apollo.MutationHookOptions; export type UpdateUserMutationResult = Apollo.MutationResult; export type UpdateUserMutationOptions = Apollo.BaseMutationOptions; +export const UploadProfilePictureDocument = gql` + mutation UploadProfilePicture($file: Upload!) { + uploadProfilePicture(file: $file) +} + `; +export type UploadProfilePictureMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UploadProfilePictureDocument, options); + } +export type UploadProfilePictureMutationHookResult = ReturnType; +export type UploadProfilePictureMutationResult = Apollo.MutationResult; +export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions; +export const RemoveProfilePictureDocument = gql` + mutation RemoveProfilePicture($where: UserWhereUniqueInput!) { + updateUser(data: {avatarUrl: {set: null}}, where: $where) { + id + } +} + `; +export type RemoveProfilePictureMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveProfilePictureDocument, options); + } +export type RemoveProfilePictureMutationHookResult = ReturnType; +export type RemoveProfilePictureMutationResult = Apollo.MutationResult; +export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions; export const GetCurrentWorkspaceDocument = gql` query GetCurrentWorkspace { currentWorkspace { diff --git a/front/src/modules/settings/profile/components/NameFields.tsx b/front/src/modules/settings/profile/components/NameFields.tsx index b84f03539..2afdae980 100644 --- a/front/src/modules/settings/profile/components/NameFields.tsx +++ b/front/src/modules/settings/profile/components/NameFields.tsx @@ -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` diff --git a/front/src/modules/settings/profile/components/PictureUploader.tsx b/front/src/modules/settings/profile/components/PictureUploader.tsx index e83b73850..c9153cc7a 100644 --- a/front/src/modules/settings/profile/components/PictureUploader.tsx +++ b/front/src/modules/settings/profile/components/PictureUploader.tsx @@ -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 ; + async function onRemove() { + await removePicture({ + variables: { + where: { + id: currentUser?.id, + }, + }, + refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''], + }); + } + + return ( + + ); } diff --git a/front/src/modules/settings/profile/queries/index.tsx b/front/src/modules/settings/profile/queries/index.tsx deleted file mode 100644 index 573fb878c..000000000 --- a/front/src/modules/settings/profile/queries/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from '@apollo/client'; - -export const UPDATE_PROFILE_PICTURE = gql` - mutation UploadProfilePicture($file: Upload!) { - uploadProfilePicture(file: $file) - } -`; diff --git a/front/src/modules/ui/components/inputs/ImageInput.tsx b/front/src/modules/ui/components/inputs/ImageInput.tsx index 6935c8aa4..10f37a85d 100644 --- a/front/src/modules/ui/components/inputs/ImageInput.tsx +++ b/front/src/modules/ui/components/inputs/ImageInput.tsx @@ -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 /> diff --git a/front/src/modules/users/components/Avatar.tsx b/front/src/modules/users/components/Avatar.tsx index aa125f557..43d59fcd3 100644 --- a/front/src/modules/users/components/Avatar.tsx +++ b/front/src/modules/users/components/Avatar.tsx @@ -12,6 +12,7 @@ type OwnProps = { }; export const StyledAvatar = styled.div>` + align-items: center; background-color: ${(props) => !isNonEmptyString(props.avatarUrl) ? props.theme.background.tertiary @@ -22,12 +23,12 @@ export const StyledAvatar = styled.div>` 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; `; diff --git a/front/src/modules/users/services/index.ts b/front/src/modules/users/queries/index.ts similarity index 100% rename from front/src/modules/users/services/index.ts rename to front/src/modules/users/queries/index.ts diff --git a/front/src/modules/users/queries/update.ts b/front/src/modules/users/queries/update.ts new file mode 100644 index 000000000..7fec81815 --- /dev/null +++ b/front/src/modules/users/queries/update.ts @@ -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 + } + } +`; diff --git a/front/src/modules/users/services/update.ts b/front/src/modules/users/services/update.ts deleted file mode 100644 index 9dbae1a9c..000000000 --- a/front/src/modules/users/services/update.ts +++ /dev/null @@ -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 - } - } -`; diff --git a/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts b/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts new file mode 100644 index 000000000..cf5034b37 --- /dev/null +++ b/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts @@ -0,0 +1,5 @@ +export function getImageAbsoluteURI(imageRelativePath?: string | null) { + return imageRelativePath + ? `${process.env.REACT_APP_FILES_URL}/${imageRelativePath}` + : null; +} diff --git a/front/src/modules/workspace/components/WorkspaceMemberCard.tsx b/front/src/modules/workspace/components/WorkspaceMemberCard.tsx index b64a55343..b13016a42 100644 --- a/front/src/modules/workspace/components/WorkspaceMemberCard.tsx +++ b/front/src/modules/workspace/components/WorkspaceMemberCard.tsx @@ -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) { = { + title: 'Pages/Auth/CreateProfile', + component: CreateProfile, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + render: getRenderWrapperForPage( + + + + + , + '/auth/create-profile', + ), + parameters: { + msw: graphqlMocks, + }, +}; diff --git a/front/src/pages/auth/__stories__/CreateWorkspace.stories.tsx b/front/src/pages/auth/__stories__/CreateWorkspace.stories.tsx new file mode 100644 index 000000000..cd35b8f3e --- /dev/null +++ b/front/src/pages/auth/__stories__/CreateWorkspace.stories.tsx @@ -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 = { + title: 'Pages/Auth/CreateWorkspace', + component: CreateWorkspace, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + render: getRenderWrapperForPage( + + + + + , + '/auth/create-workspace', + ), + parameters: { + msw: graphqlMocks, + }, +}; diff --git a/front/src/pages/opportunities/__stories__/Opportunities.mdx b/front/src/pages/opportunities/__stories__/Opportunities.mdx new file mode 100644 index 000000000..74e37e19d --- /dev/null +++ b/front/src/pages/opportunities/__stories__/Opportunities.mdx @@ -0,0 +1,11 @@ +{ /* Opportunities.mdx */ } + +import { Canvas, Meta } from '@storybook/blocks'; + +import * as Opportunities from './Opportunities.stories'; + + + +# Opportunities View + + \ No newline at end of file diff --git a/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx b/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx new file mode 100644 index 000000000..1f2c81657 --- /dev/null +++ b/front/src/pages/opportunities/__stories__/Opportunities.stories.tsx @@ -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 = { + title: 'Pages/Opportunities', + component: Opportunities, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + render: getRenderWrapperForPage(, '/opportunities'), + parameters: { + msw: graphqlMocks, + }, +}; diff --git a/front/src/pages/people/__stories__/People.filterBy.stories.tsx b/front/src/pages/people/__stories__/People.filterBy.stories.tsx index d95107b93..b16e1a48e 100644 --- a/front/src/pages/people/__stories__/People.filterBy.stories.tsx +++ b/front/src/pages/people/__stories__/People.filterBy.stories.tsx @@ -77,7 +77,7 @@ export const CompanyName: Story = { delay: 200, }); - await sleep(1000); + await sleep(500); const qontoChip = canvas .getAllByTestId('dropdown-menu-item') diff --git a/front/src/pages/people/__stories__/People.inputs.stories.tsx b/front/src/pages/people/__stories__/People.inputs.stories.tsx index bfa9fc4a6..ae5a04604 100644 --- a/front/src/pages/people/__stories__/People.inputs.stories.tsx +++ b/front/src/pages/people/__stories__/People.inputs.stories.tsx @@ -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'); diff --git a/front/src/testing/graphqlMocks.ts b/front/src/testing/graphqlMocks.ts index 9b1451eff..c6506317c 100644 --- a/front/src/testing/graphqlMocks.ts +++ b/front/src/testing/graphqlMocks.ts @@ -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 }, + }, + }), + ); + }), ]; diff --git a/front/src/testing/mock-data/pipelines.ts b/front/src/testing/mock-data/pipelines.ts new file mode 100644 index 000000000..817634b39 --- /dev/null +++ b/front/src/testing/mock-data/pipelines.ts @@ -0,0 +1,90 @@ +import { + Pipeline, + PipelineProgress, + PipelineProgressableType, + PipelineStage, +} from '../../generated/graphql'; + +type MockedPipeline = Pick< + Pipeline, + 'id' | 'name' | 'pipelineProgressableType' | '__typename' +> & { + pipelineStages: Array< + Pick & { + pipelineProgresses: Array< + Pick< + PipelineProgress, + | 'id' + | 'progressableType' + | 'progressableId' + | 'amount' + | 'closeDate' + | '__typename' + > + >; + } + >; +}; + +export const mockedPipelinesData: Array = [ + { + 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', + }, +]; diff --git a/server/src/core/auth/auth.resolver.ts b/server/src/core/auth/auth.resolver.ts index d84dde440..d4eea4f46 100644 --- a/server/src/core/auth/auth.resolver.ts +++ b/server/src/core/auth/auth.resolver.ts @@ -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';