diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 5ad9319b2..0605f4080 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1112,7 +1112,8 @@ export type EnumPipelineProgressableTypeFilter = { }; export enum FileFolder { - ProfilePicture = 'ProfilePicture' + ProfilePicture = 'ProfilePicture', + WorkspaceLogo = 'WorkspaceLogo' } export type IntNullableFilter = { @@ -1160,6 +1161,7 @@ export type Mutation = { deleteManyCompany: AffectedRows; deleteManyPerson: AffectedRows; deleteManyPipelineProgress: AffectedRows; + deleteWorkspaceMember: WorkspaceMember; renewToken: AuthTokens; updateOneCommentThread: CommentThread; updateOneCompany?: Maybe; @@ -1232,6 +1234,11 @@ export type MutationDeleteManyPipelineProgressArgs = { }; +export type MutationDeleteWorkspaceMemberArgs = { + where: WorkspaceMemberWhereUniqueInput; +}; + + export type MutationRenewTokenArgs = { refreshToken: Scalars['String']; }; @@ -2430,6 +2437,7 @@ export type Query = { findManyPipelineProgress: Array; findManyPipelineStage: Array; findManyUser: Array; + findManyWorkspaceMember: Array; findUniqueCompany: Company; findUniquePerson: Person; }; @@ -2510,6 +2518,16 @@ export type QueryFindManyUserArgs = { }; +export type QueryFindManyWorkspaceMemberArgs = { + cursor?: InputMaybe; + distinct?: InputMaybe>; + orderBy?: InputMaybe>; + skip?: InputMaybe; + take?: InputMaybe; + where?: InputMaybe; +}; + + export type QueryFindUniqueCompanyArgs = { id: Scalars['String']; }; @@ -2930,6 +2948,23 @@ export type WorkspaceMemberCreateWithoutWorkspaceInput = { user: UserCreateNestedOneWithoutWorkspaceMemberInput; }; +export type WorkspaceMemberOrderByWithRelationInput = { + createdAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; + user?: InputMaybe; + userId?: InputMaybe; +}; + +export enum WorkspaceMemberScalarFieldEnum { + CreatedAt = 'createdAt', + DeletedAt = 'deletedAt', + Id = 'id', + UpdatedAt = 'updatedAt', + UserId = 'userId', + WorkspaceId = 'workspaceId' +} + export type WorkspaceMemberScalarWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -2983,6 +3018,17 @@ export type WorkspaceMemberUpsertWithWhereUniqueWithoutWorkspaceInput = { where: WorkspaceMemberWhereUniqueInput; }; +export type WorkspaceMemberWhereInput = { + AND?: InputMaybe>; + NOT?: InputMaybe>; + OR?: InputMaybe>; + createdAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; + user?: InputMaybe; + userId?: InputMaybe; +}; + export type WorkspaceMemberWhereUniqueInput = { id?: InputMaybe; userId?: InputMaybe; @@ -3330,10 +3376,10 @@ export type RemoveProfilePictureMutationVariables = Exact<{ export type RemoveProfilePictureMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string } }; -export type GetCurrentWorkspaceQueryVariables = Exact<{ [key: string]: never; }>; +export type GetWorkspaceMembersQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentWorkspaceQuery = { __typename?: 'Query', currentWorkspace: { __typename?: 'Workspace', id: string, workspaceMember?: Array<{ __typename?: 'WorkspaceMember', id: string, user: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, firstName?: string | null, lastName?: string | null } }> | null } }; +export type GetWorkspaceMembersQuery = { __typename?: 'Query', workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, user: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, firstName?: string | null, lastName?: string | null } }> }; export type UpdateWorkspaceMutationVariables = Exact<{ data: WorkspaceUpdateInput; @@ -3354,6 +3400,13 @@ export type RemoveWorkspaceLogoMutationVariables = Exact<{ [key: string]: never; export type RemoveWorkspaceLogoMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string } }; +export type RemoveWorkspaceMemberMutationVariables = Exact<{ + where: WorkspaceMemberWhereUniqueInput; +}>; + + +export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', deleteWorkspaceMember: { __typename?: 'WorkspaceMember', id: string } }; + export const CreateEventDocument = gql` mutation CreateEvent($type: String!, $data: JSON!) { @@ -5041,50 +5094,47 @@ export function useRemoveProfilePictureMutation(baseOptions?: Apollo.MutationHoo export type RemoveProfilePictureMutationHookResult = ReturnType; export type RemoveProfilePictureMutationResult = Apollo.MutationResult; export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions; -export const GetCurrentWorkspaceDocument = gql` - query GetCurrentWorkspace { - currentWorkspace { +export const GetWorkspaceMembersDocument = gql` + query GetWorkspaceMembers { + workspaceMembers: findManyWorkspaceMember { id - workspaceMember { + user { id - user { - id - email - avatarUrl - firstName - lastName - } + email + avatarUrl + firstName + lastName } } } `; /** - * __useGetCurrentWorkspaceQuery__ + * __useGetWorkspaceMembersQuery__ * - * To run a query within a React component, call `useGetCurrentWorkspaceQuery` and pass it any options that fit your needs. - * When your component renders, `useGetCurrentWorkspaceQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useGetWorkspaceMembersQuery` and pass it any options that fit your needs. + * When your component renders, `useGetWorkspaceMembersQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useGetCurrentWorkspaceQuery({ + * const { data, loading, error } = useGetWorkspaceMembersQuery({ * variables: { * }, * }); */ -export function useGetCurrentWorkspaceQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useGetWorkspaceMembersQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetCurrentWorkspaceDocument, options); + return Apollo.useQuery(GetWorkspaceMembersDocument, options); } -export function useGetCurrentWorkspaceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useGetWorkspaceMembersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetCurrentWorkspaceDocument, options); + return Apollo.useLazyQuery(GetWorkspaceMembersDocument, options); } -export type GetCurrentWorkspaceQueryHookResult = ReturnType; -export type GetCurrentWorkspaceLazyQueryHookResult = ReturnType; -export type GetCurrentWorkspaceQueryResult = Apollo.QueryResult; +export type GetWorkspaceMembersQueryHookResult = ReturnType; +export type GetWorkspaceMembersLazyQueryHookResult = ReturnType; +export type GetWorkspaceMembersQueryResult = Apollo.QueryResult; export const UpdateWorkspaceDocument = gql` mutation UpdateWorkspace($data: WorkspaceUpdateInput!) { updateWorkspace(data: $data) { @@ -5183,4 +5233,37 @@ export function useRemoveWorkspaceLogoMutation(baseOptions?: Apollo.MutationHook } export type RemoveWorkspaceLogoMutationHookResult = ReturnType; export type RemoveWorkspaceLogoMutationResult = Apollo.MutationResult; -export type RemoveWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file +export type RemoveWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions; +export const RemoveWorkspaceMemberDocument = gql` + mutation RemoveWorkspaceMember($where: WorkspaceMemberWhereUniqueInput!) { + deleteWorkspaceMember(where: $where) { + id + } +} + `; +export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction; + +/** + * __useRemoveWorkspaceMemberMutation__ + * + * To run a mutation, you first call `useRemoveWorkspaceMemberMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveWorkspaceMemberMutation` 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 [removeWorkspaceMemberMutation, { data, loading, error }] = useRemoveWorkspaceMemberMutation({ + * variables: { + * where: // value for 'where' + * }, + * }); + */ +export function useRemoveWorkspaceMemberMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveWorkspaceMemberDocument, options); + } +export type RemoveWorkspaceMemberMutationHookResult = ReturnType; +export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult; +export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/front/src/modules/apollo/hooks/useApolloFactory.ts b/front/src/modules/apollo/hooks/useApolloFactory.ts index 5beb22560..b3b857465 100644 --- a/front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/front/src/modules/apollo/hooks/useApolloFactory.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { useRecoilState } from 'recoil'; @@ -38,6 +38,8 @@ export function useApolloFactory() { fetchPolicy: 'cache-first', }, }, + // We don't want to re-create the client on token change or it will cause infinite loop + initialTokenPair: tokenPair, onTokenPairChange(tokenPair) { setTokenPair(tokenPair); }, @@ -46,11 +48,17 @@ export function useApolloFactory() { }, extraLinks: [], isDebugMode, - tokenPair, }); return apolloRef.current.getClient(); - }, [setTokenPair, isDebugMode, tokenPair]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setTokenPair, isDebugMode]); + + useEffect(() => { + if (apolloRef.current) { + apolloRef.current.updateTokenPair(tokenPair); + } + }, [tokenPair]); return apolloClient; } diff --git a/front/src/modules/apollo/services/apollo.factory.ts b/front/src/modules/apollo/services/apollo.factory.ts index c4c76f653..342b2d954 100644 --- a/front/src/modules/apollo/services/apollo.factory.ts +++ b/front/src/modules/apollo/services/apollo.factory.ts @@ -28,9 +28,9 @@ export interface Options extends ApolloClientOptions { onNetworkError?: (err: Error | ServerParseError | ServerError) => void; onTokenPairChange?: (tokenPair: AuthTokenPair) => void; onUnauthenticatedError?: () => void; + initialTokenPair: AuthTokenPair | null; extraLinks?: ApolloLink[]; isDebugMode?: boolean; - tokenPair: AuthTokenPair | null; } export class ApolloFactory implements ApolloManager { @@ -44,13 +44,13 @@ export class ApolloFactory implements ApolloManager { onNetworkError, onTokenPairChange, onUnauthenticatedError, + initialTokenPair, extraLinks, isDebugMode, - tokenPair, ...options } = opts; - this.tokenPair = tokenPair; + this.tokenPair = initialTokenPair; const buildApolloLink = (): ApolloLink => { const httpLink = createUploadLink({ diff --git a/front/src/modules/ui/components/buttons/Button.tsx b/front/src/modules/ui/components/buttons/Button.tsx index 7435be952..45d70ae2d 100644 --- a/front/src/modules/ui/components/buttons/Button.tsx +++ b/front/src/modules/ui/components/buttons/Button.tsx @@ -15,14 +15,14 @@ type Size = 'medium' | 'small'; type Props = { icon?: React.ReactNode; - title: string; + title?: string; fullWidth?: boolean; variant?: Variant; size?: Size; } & React.ComponentProps<'button'>; const StyledButton = styled.button< - Pick + Pick >` align-items: center; background: ${({ theme, variant, disabled }) => { @@ -33,8 +33,10 @@ const StyledButton = styled.button< } else { return theme.color.blue; } - default: + case 'secondary': return theme.background.primary; + default: + return 'transparent'; } }}; border: ${({ theme, variant }) => { @@ -93,7 +95,13 @@ const StyledButton = styled.button< gap: ${({ theme }) => theme.spacing(2)}; height: ${({ size }) => (size === 'small' ? '24px' : '32px')}; justify-content: flex-start; - padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)}; + padding: ${({ theme, title }) => { + if (!title) { + return `${theme.spacing(1)}`; + } + + return `${theme.spacing(2)} ${theme.spacing(3)}`; + }}; transition: background 0.1s ease; @@ -143,6 +151,7 @@ export function Button({ fullWidth={fullWidth} variant={variant} size={size} + title={title} {...props} > {icon} diff --git a/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts b/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts index 58bb79bb1..0a8da6b3f 100644 --- a/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts +++ b/front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts @@ -6,5 +6,6 @@ export function getImageAbsoluteURIOrBase64(imageUrl?: string | null) { if (imageUrl?.startsWith('data:')) { return imageUrl; } + return `${process.env.REACT_APP_FILES_URL}/${imageUrl}`; } diff --git a/front/src/modules/workspace/components/WorkspaceMemberCard.tsx b/front/src/modules/workspace/components/WorkspaceMemberCard.tsx index db98967f9..74cc05f05 100644 --- a/front/src/modules/workspace/components/WorkspaceMemberCard.tsx +++ b/front/src/modules/workspace/components/WorkspaceMemberCard.tsx @@ -12,21 +12,15 @@ const StyledContainer = styled.div` flex-direction: row; margin-bottom: ${({ theme }) => theme.spacing(0)}; margin-top: ${({ theme }) => theme.spacing(4)}; + padding: ${({ theme }) => theme.spacing(3)}; `; -const AvatarContainer = styled.div` - margin: ${({ theme }) => theme.spacing(3)}; -`; - -const TextContainer = styled.div` +const Content = styled.div` display: flex; + flex: 1; flex-direction: column; justify-content: center; -`; - -const NameAndEmailContainer = styled.div` - display: flex; - flex-direction: column; + margin-left: ${({ theme }) => theme.spacing(3)}; `; const NameText = styled.span` @@ -41,29 +35,26 @@ type OwnProps = { workspaceMember: { user: Pick; }; + accessory?: React.ReactNode; }; -export function WorkspaceMemberCard({ workspaceMember }: OwnProps) { +export function WorkspaceMemberCard({ workspaceMember, accessory }: OwnProps) { return ( - - - - - - - {workspaceMember.user.firstName} {workspaceMember.user.lastName}{' '} - - {workspaceMember.user.email} - - + + + + {workspaceMember.user.firstName} {workspaceMember.user.lastName}{' '} + + {workspaceMember.user.email} + + + {accessory} ); } diff --git a/front/src/modules/workspace/queries/select.ts b/front/src/modules/workspace/queries/select.ts index 168d49723..d0ed31436 100644 --- a/front/src/modules/workspace/queries/select.ts +++ b/front/src/modules/workspace/queries/select.ts @@ -1,18 +1,15 @@ import { gql } from '@apollo/client'; -export const GET_CURRENT_WORKSPACE = gql` - query GetCurrentWorkspace { - currentWorkspace { +export const GET_WORKSPACE_MEMBERS = gql` + query GetWorkspaceMembers { + workspaceMembers: findManyWorkspaceMember { id - workspaceMember { + user { id - user { - id - email - avatarUrl - firstName - lastName - } + email + avatarUrl + firstName + lastName } } } diff --git a/front/src/modules/workspace/queries/update.ts b/front/src/modules/workspace/queries/update.ts index 9e2f747ed..ca11c29d5 100644 --- a/front/src/modules/workspace/queries/update.ts +++ b/front/src/modules/workspace/queries/update.ts @@ -24,3 +24,11 @@ export const REMOVE_WORKSPACE_LOGO = gql` } } `; + +export const REMOVE_WORKSPACE_MEMBER = gql` + mutation RemoveWorkspaceMember($where: WorkspaceMemberWhereUniqueInput!) { + deleteWorkspaceMember(where: $where) { + id + } + } +`; diff --git a/front/src/pages/settings/SettingsWorkspaceMembers.tsx b/front/src/pages/settings/SettingsWorkspaceMembers.tsx index a349b8373..b8ee1e349 100644 --- a/front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -1,10 +1,17 @@ import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { Button } from '@/ui/components/buttons/Button'; import { MainSectionTitle } from '@/ui/components/section-titles/MainSectionTitle'; import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle'; +import { IconTrash } from '@/ui/icons'; import { NoTopBarContainer } from '@/ui/layout/containers/NoTopBarContainer'; import { WorkspaceMemberCard } from '@/workspace/components/WorkspaceMemberCard'; -import { useGetCurrentWorkspaceQuery } from '~/generated/graphql'; +import { + useGetWorkspaceMembersQuery, + useRemoveWorkspaceMemberMutation, +} from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; @@ -16,8 +23,52 @@ const StyledContainer = styled.div` } `; +const ButtonContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + margin-left: ${({ theme }) => theme.spacing(3)}; +`; + export function SettingsWorkspaceMembers() { - const { data } = useGetCurrentWorkspaceQuery(); + const [currentUser] = useRecoilState(currentUserState); + + const { data } = useGetWorkspaceMembersQuery(); + + const [removeWorkspaceMember] = useRemoveWorkspaceMemberMutation(); + + const handleRemoveWorkspaceMember = async (userId: string) => { + await removeWorkspaceMember({ + variables: { + where: { + userId, + }, + }, + optimisticResponse: { + __typename: 'Mutation', + deleteWorkspaceMember: { + __typename: 'WorkspaceMember', + id: userId, + }, + }, + update: (cache, { data: responseData }) => { + if (!responseData) { + return; + } + + const normalizedId = cache.identify({ + id: responseData.deleteWorkspaceMember.id, + __typename: 'WorkspaceMember', + }); + + // Evict object from cache + cache.evict({ id: normalizedId }); + + // Clean up relation to this object + cache.gc(); + }, + }); + }; return ( @@ -27,10 +78,22 @@ export function SettingsWorkspaceMembers() { title="Members" description="Manage the members of your space here" /> - {data?.currentWorkspace?.workspaceMember?.map((member) => ( + {data?.workspaceMembers?.map((member) => ( +