From d28a762661e453239faaf024c5f2f3a7fe889871 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 15 Jun 2023 16:53:59 +0200 Subject: [PATCH] Finished relation picker for companies (#305) * Finished relation picker for companies * Minor fixes --- front/src/apollo.tsx | 23 +- front/src/generated/graphql.tsx | 470 +++++++++++++++++- .../comments/CommentThreadRelationPicker.tsx | 214 +++++--- front/src/modules/comments/services/select.ts | 1 + front/src/modules/comments/services/update.ts | 62 +++ .../companies/components/CompanyChip.tsx | 2 + front/src/modules/search/services/search.ts | 12 +- .../menu/DropdownMenuItemContainer.tsx | 3 + .../hooks/useListenClickOutsideArrayOfRef.ts | 2 +- .../api/resolvers/comment-thread.resolver.ts | 15 + 10 files changed, 741 insertions(+), 63 deletions(-) create mode 100644 front/src/modules/comments/services/update.ts diff --git a/front/src/apollo.tsx b/front/src/apollo.tsx index 80aa3ee10..545e63f78 100644 --- a/front/src/apollo.tsx +++ b/front/src/apollo.tsx @@ -9,6 +9,7 @@ import { setContext } from '@apollo/client/link/context'; import { onError } from '@apollo/client/link/error'; import { RestLink } from 'apollo-link-rest'; +import { CommentThreadTarget } from './generated/graphql'; import { refreshAccessToken } from './modules/auth/services/AuthService'; const apiLink = createHttpLink({ @@ -65,7 +66,27 @@ const errorLink = onError(({ graphQLErrors, operation, forward }) => { export const apiClient = new ApolloClient({ link: from([errorLink, withAuthHeadersLink, apiLink]), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + typePolicies: { + CommentThread: { + fields: { + commentThreadTargets: { + merge( + existing: CommentThreadTarget[] = [], + incoming: CommentThreadTarget[], + ) { + return [...incoming]; + }, + }, + }, + }, + }, + }), + defaultOptions: { + query: { + fetchPolicy: 'cache-first', + }, + }, }); const authLink = new RestLink({ diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 80feb937c..307ebe0e0 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -22,6 +22,10 @@ export type AffectedRows = { count: Scalars['Int']; }; +export type BoolFieldUpdateOperationsInput = { + set?: InputMaybe; +}; + export type BoolFilter = { equals?: InputMaybe; not?: InputMaybe; @@ -68,6 +72,20 @@ export type CommentCreateNestedManyWithoutCommentThreadInput = { createMany?: InputMaybe; }; +export type CommentCreateOrConnectWithoutCommentThreadInput = { + create: CommentCreateWithoutCommentThreadInput; + where: CommentWhereUniqueInput; +}; + +export type CommentCreateWithoutCommentThreadInput = { + author: UserCreateNestedOneWithoutCommentsInput; + body: Scalars['String']; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id: Scalars['String']; + updatedAt?: InputMaybe; +}; + export type CommentListRelationFilter = { every?: InputMaybe; none?: InputMaybe; @@ -78,6 +96,19 @@ export type CommentOrderByRelationAggregateInput = { _count?: InputMaybe; }; +export type CommentScalarWhereInput = { + AND?: InputMaybe>; + NOT?: InputMaybe>; + OR?: InputMaybe>; + authorId?: InputMaybe; + body?: InputMaybe; + commentThreadId?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type CommentThread = { __typename?: 'CommentThread'; commentThreadTargets?: Maybe>; @@ -153,6 +184,20 @@ export type CommentThreadTargetCreateNestedManyWithoutCommentThreadInput = { createMany?: InputMaybe; }; +export type CommentThreadTargetCreateOrConnectWithoutCommentThreadInput = { + create: CommentThreadTargetCreateWithoutCommentThreadInput; + where: CommentThreadTargetWhereUniqueInput; +}; + +export type CommentThreadTargetCreateWithoutCommentThreadInput = { + commentableId: Scalars['String']; + commentableType: CommentableType; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id: Scalars['String']; + updatedAt?: InputMaybe; +}; + export type CommentThreadTargetListRelationFilter = { every?: InputMaybe; none?: InputMaybe; @@ -163,6 +208,67 @@ export type CommentThreadTargetOrderByRelationAggregateInput = { _count?: InputMaybe; }; +export type CommentThreadTargetScalarWhereInput = { + AND?: InputMaybe>; + NOT?: InputMaybe>; + OR?: InputMaybe>; + commentThreadId?: InputMaybe; + commentableId?: InputMaybe; + commentableType?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CommentThreadTargetUpdateManyMutationInput = { + commentableId?: InputMaybe; + commentableType?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CommentThreadTargetUpdateManyWithWhereWithoutCommentThreadInput = { + data: CommentThreadTargetUpdateManyMutationInput; + where: CommentThreadTargetScalarWhereInput; +}; + +export type CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput = { + connect?: InputMaybe>; + connectOrCreate?: InputMaybe>; + create?: InputMaybe>; + createMany?: InputMaybe; + delete?: InputMaybe>; + deleteMany?: InputMaybe>; + disconnect?: InputMaybe>; + set?: InputMaybe>; + update?: InputMaybe>; + updateMany?: InputMaybe>; + upsert?: InputMaybe>; +}; + +export type CommentThreadTargetUpdateWithWhereUniqueWithoutCommentThreadInput = { + data: CommentThreadTargetUpdateWithoutCommentThreadInput; + where: CommentThreadTargetWhereUniqueInput; +}; + +export type CommentThreadTargetUpdateWithoutCommentThreadInput = { + commentableId?: InputMaybe; + commentableType?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CommentThreadTargetUpsertWithWhereUniqueWithoutCommentThreadInput = { + create: CommentThreadTargetCreateWithoutCommentThreadInput; + update: CommentThreadTargetUpdateWithoutCommentThreadInput; + where: CommentThreadTargetWhereUniqueInput; +}; + export type CommentThreadTargetWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -177,6 +283,19 @@ export type CommentThreadTargetWhereInput = { updatedAt?: InputMaybe; }; +export type CommentThreadTargetWhereUniqueInput = { + id?: InputMaybe; +}; + +export type CommentThreadUpdateInput = { + commentThreadTargets?: InputMaybe; + comments?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type CommentThreadWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -193,6 +312,53 @@ export type CommentThreadWhereUniqueInput = { id?: InputMaybe; }; +export type CommentUpdateManyMutationInput = { + body?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CommentUpdateManyWithWhereWithoutCommentThreadInput = { + data: CommentUpdateManyMutationInput; + where: CommentScalarWhereInput; +}; + +export type CommentUpdateManyWithoutCommentThreadNestedInput = { + connect?: InputMaybe>; + connectOrCreate?: InputMaybe>; + create?: InputMaybe>; + createMany?: InputMaybe; + delete?: InputMaybe>; + deleteMany?: InputMaybe>; + disconnect?: InputMaybe>; + set?: InputMaybe>; + update?: InputMaybe>; + updateMany?: InputMaybe>; + upsert?: InputMaybe>; +}; + +export type CommentUpdateWithWhereUniqueWithoutCommentThreadInput = { + data: CommentUpdateWithoutCommentThreadInput; + where: CommentWhereUniqueInput; +}; + +export type CommentUpdateWithoutCommentThreadInput = { + author?: InputMaybe; + body?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + id?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CommentUpsertWithWhereUniqueWithoutCommentThreadInput = { + create: CommentCreateWithoutCommentThreadInput; + update: CommentUpdateWithoutCommentThreadInput; + where: CommentWhereUniqueInput; +}; + export type CommentWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -208,6 +374,10 @@ export type CommentWhereInput = { updatedAt?: InputMaybe; }; +export type CommentWhereUniqueInput = { + id?: InputMaybe; +}; + export enum CommentableType { Company = 'Company', Person = 'Person' @@ -244,10 +414,50 @@ export type CompanyCreateInput = { updatedAt?: InputMaybe; }; +export type CompanyCreateManyAccountOwnerInput = { + address: Scalars['String']; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + domainName: Scalars['String']; + employees?: InputMaybe; + id: Scalars['String']; + name: Scalars['String']; + updatedAt?: InputMaybe; +}; + +export type CompanyCreateManyAccountOwnerInputEnvelope = { + data: Array; + skipDuplicates?: InputMaybe; +}; + +export type CompanyCreateNestedManyWithoutAccountOwnerInput = { + connect?: InputMaybe>; + connectOrCreate?: InputMaybe>; + create?: InputMaybe>; + createMany?: InputMaybe; +}; + export type CompanyCreateNestedOneWithoutPeopleInput = { connect?: InputMaybe; }; +export type CompanyCreateOrConnectWithoutAccountOwnerInput = { + create: CompanyCreateWithoutAccountOwnerInput; + where: CompanyWhereUniqueInput; +}; + +export type CompanyCreateWithoutAccountOwnerInput = { + address: Scalars['String']; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + domainName: Scalars['String']; + employees?: InputMaybe; + id: Scalars['String']; + name: Scalars['String']; + people?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type CompanyListRelationFilter = { every?: InputMaybe; none?: InputMaybe; @@ -290,6 +500,21 @@ export enum CompanyScalarFieldEnum { WorkspaceId = 'workspaceId' } +export type CompanyScalarWhereInput = { + AND?: InputMaybe>; + NOT?: InputMaybe>; + OR?: InputMaybe>; + accountOwnerId?: InputMaybe; + address?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + domainName?: InputMaybe; + employees?: InputMaybe; + id?: InputMaybe; + name?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type CompanyUpdateInput = { accountOwner?: InputMaybe; address?: InputMaybe; @@ -303,10 +528,63 @@ export type CompanyUpdateInput = { updatedAt?: InputMaybe; }; +export type CompanyUpdateManyMutationInput = { + address?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + domainName?: InputMaybe; + employees?: InputMaybe; + id?: InputMaybe; + name?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CompanyUpdateManyWithWhereWithoutAccountOwnerInput = { + data: CompanyUpdateManyMutationInput; + where: CompanyScalarWhereInput; +}; + +export type CompanyUpdateManyWithoutAccountOwnerNestedInput = { + connect?: InputMaybe>; + connectOrCreate?: InputMaybe>; + create?: InputMaybe>; + createMany?: InputMaybe; + delete?: InputMaybe>; + deleteMany?: InputMaybe>; + disconnect?: InputMaybe>; + set?: InputMaybe>; + update?: InputMaybe>; + updateMany?: InputMaybe>; + upsert?: InputMaybe>; +}; + export type CompanyUpdateOneWithoutPeopleNestedInput = { connect?: InputMaybe; }; +export type CompanyUpdateWithWhereUniqueWithoutAccountOwnerInput = { + data: CompanyUpdateWithoutAccountOwnerInput; + where: CompanyWhereUniqueInput; +}; + +export type CompanyUpdateWithoutAccountOwnerInput = { + address?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + domainName?: InputMaybe; + employees?: InputMaybe; + id?: InputMaybe; + name?: InputMaybe; + people?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CompanyUpsertWithWhereUniqueWithoutAccountOwnerInput = { + create: CompanyCreateWithoutAccountOwnerInput; + update: CompanyUpdateWithoutAccountOwnerInput; + where: CompanyWhereUniqueInput; +}; + export type CompanyWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -354,6 +632,10 @@ export type DateTimeNullableFilter = { notIn?: InputMaybe>; }; +export type EnumCommentableTypeFieldUpdateOperationsInput = { + set?: InputMaybe; +}; + export type EnumCommentableTypeFilter = { equals?: InputMaybe; in?: InputMaybe>; @@ -409,6 +691,7 @@ export type Mutation = { deleteManyCompany: AffectedRows; deleteManyPerson: AffectedRows; deleteManyPipelineProgress: AffectedRows; + updateOneCommentThread: CommentThread; updateOneCompany?: Maybe; updateOnePerson?: Maybe; updateOnePipelineProgress?: Maybe; @@ -455,6 +738,12 @@ export type MutationDeleteManyPipelineProgressArgs = { }; +export type MutationUpdateOneCommentThreadArgs = { + data: CommentThreadUpdateInput; + where: CommentThreadWhereUniqueInput; +}; + + export type MutationUpdateOneCompanyArgs = { data: CompanyUpdateInput; where: CompanyWhereUniqueInput; @@ -564,6 +853,10 @@ export type NullableIntFieldUpdateOperationsInput = { set?: InputMaybe; }; +export type NullableStringFieldUpdateOperationsInput = { + set?: InputMaybe; +}; + export type Person = { __typename?: 'Person'; _commentCount: Scalars['Int']; @@ -1081,6 +1374,29 @@ export type UserCreateNestedOneWithoutCompaniesInput = { connect?: InputMaybe; }; +export type UserCreateOrConnectWithoutCommentsInput = { + create: UserCreateWithoutCommentsInput; + where: UserWhereUniqueInput; +}; + +export type UserCreateWithoutCommentsInput = { + avatarUrl?: InputMaybe; + companies?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + disabled?: InputMaybe; + displayName: Scalars['String']; + email: Scalars['String']; + emailVerified?: InputMaybe; + id: Scalars['String']; + lastSeen?: InputMaybe; + locale: Scalars['String']; + metadata?: InputMaybe; + passwordHash?: InputMaybe; + phoneNumber?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type UserOrderByWithRelationInput = { avatarUrl?: InputMaybe; comments?: InputMaybe; @@ -1122,10 +1438,41 @@ export enum UserScalarFieldEnum { UpdatedAt = 'updatedAt' } +export type UserUpdateOneRequiredWithoutCommentsNestedInput = { + connect?: InputMaybe; + connectOrCreate?: InputMaybe; + create?: InputMaybe; + update?: InputMaybe; + upsert?: InputMaybe; +}; + export type UserUpdateOneWithoutCompaniesNestedInput = { connect?: InputMaybe; }; +export type UserUpdateWithoutCommentsInput = { + avatarUrl?: InputMaybe; + companies?: InputMaybe; + createdAt?: InputMaybe; + deletedAt?: InputMaybe; + disabled?: InputMaybe; + displayName?: InputMaybe; + email?: InputMaybe; + emailVerified?: InputMaybe; + id?: InputMaybe; + lastSeen?: InputMaybe; + locale?: InputMaybe; + metadata?: InputMaybe; + passwordHash?: InputMaybe; + phoneNumber?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type UserUpsertWithoutCommentsInput = { + create: UserCreateWithoutCommentsInput; + update: UserUpdateWithoutCommentsInput; +}; + export type UserWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; @@ -1212,7 +1559,7 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{ }>; -export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', commentableId: string, commentableType: CommentableType }> | null }> }; +export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, commentableId: string, commentableType: CommentableType }> | null }> }; export type GetCommentThreadQueryVariables = Exact<{ commentThreadId: Scalars['String']; @@ -1221,6 +1568,25 @@ export type GetCommentThreadQueryVariables = Exact<{ export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', commentableId: string, commentableType: CommentableType }> | null }> }; +export type AddCommentThreadTargetOnCommentThreadMutationVariables = Exact<{ + commentThreadId: Scalars['String']; + commentThreadTargetCreationDate: Scalars['DateTime']; + commentThreadTargetId: Scalars['String']; + commentableEntityId: Scalars['String']; + commentableEntityType: CommentableType; +}>; + + +export type AddCommentThreadTargetOnCommentThreadMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, createdAt: string, updatedAt: string, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, createdAt: string, updatedAt: string, commentableType: CommentableType, commentableId: string }> | null } }; + +export type RemoveCommentThreadTargetOnCommentThreadMutationVariables = Exact<{ + commentThreadId: Scalars['String']; + commentThreadTargetId: Scalars['String']; +}>; + + +export type RemoveCommentThreadTargetOnCommentThreadMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, createdAt: string, updatedAt: string, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, createdAt: string, updatedAt: string, commentableType: CommentableType, commentableId: string }> | null } }; + export type GetCompaniesQueryVariables = Exact<{ orderBy?: InputMaybe | CompanyOrderByWithRelationInput>; where?: InputMaybe; @@ -1333,6 +1699,7 @@ export type EmptyQueryQuery = { __typename?: 'Query', searchResults: Array<{ __t export type SearchCompanyQueryQueryVariables = Exact<{ where?: InputMaybe; limit?: InputMaybe; + orderBy?: InputMaybe | CompanyOrderByWithRelationInput>; }>; @@ -1476,6 +1843,7 @@ export const GetCommentThreadsByTargetsDocument = gql` } } commentThreadTargets { + id commentableId commentableType } @@ -1561,6 +1929,101 @@ export function useGetCommentThreadLazyQuery(baseOptions?: Apollo.LazyQueryHookO export type GetCommentThreadQueryHookResult = ReturnType; export type GetCommentThreadLazyQueryHookResult = ReturnType; export type GetCommentThreadQueryResult = Apollo.QueryResult; +export const AddCommentThreadTargetOnCommentThreadDocument = gql` + mutation AddCommentThreadTargetOnCommentThread($commentThreadId: String!, $commentThreadTargetCreationDate: DateTime!, $commentThreadTargetId: String!, $commentableEntityId: String!, $commentableEntityType: CommentableType!) { + updateOneCommentThread( + where: {id: $commentThreadId} + data: {commentThreadTargets: {connectOrCreate: {create: {id: $commentThreadTargetId, createdAt: $commentThreadTargetCreationDate, commentableType: $commentableEntityType, commentableId: $commentableEntityId}, where: {id: $commentThreadTargetId}}}} + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentableType + commentableId + } + } +} + `; +export type AddCommentThreadTargetOnCommentThreadMutationFn = Apollo.MutationFunction; + +/** + * __useAddCommentThreadTargetOnCommentThreadMutation__ + * + * To run a mutation, you first call `useAddCommentThreadTargetOnCommentThreadMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddCommentThreadTargetOnCommentThreadMutation` 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 [addCommentThreadTargetOnCommentThreadMutation, { data, loading, error }] = useAddCommentThreadTargetOnCommentThreadMutation({ + * variables: { + * commentThreadId: // value for 'commentThreadId' + * commentThreadTargetCreationDate: // value for 'commentThreadTargetCreationDate' + * commentThreadTargetId: // value for 'commentThreadTargetId' + * commentableEntityId: // value for 'commentableEntityId' + * commentableEntityType: // value for 'commentableEntityType' + * }, + * }); + */ +export function useAddCommentThreadTargetOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(AddCommentThreadTargetOnCommentThreadDocument, options); + } +export type AddCommentThreadTargetOnCommentThreadMutationHookResult = ReturnType; +export type AddCommentThreadTargetOnCommentThreadMutationResult = Apollo.MutationResult; +export type AddCommentThreadTargetOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; +export const RemoveCommentThreadTargetOnCommentThreadDocument = gql` + mutation RemoveCommentThreadTargetOnCommentThread($commentThreadId: String!, $commentThreadTargetId: String!) { + updateOneCommentThread( + where: {id: $commentThreadId} + data: {commentThreadTargets: {delete: {id: $commentThreadTargetId}}} + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentableType + commentableId + } + } +} + `; +export type RemoveCommentThreadTargetOnCommentThreadMutationFn = Apollo.MutationFunction; + +/** + * __useRemoveCommentThreadTargetOnCommentThreadMutation__ + * + * To run a mutation, you first call `useRemoveCommentThreadTargetOnCommentThreadMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveCommentThreadTargetOnCommentThreadMutation` 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 [removeCommentThreadTargetOnCommentThreadMutation, { data, loading, error }] = useRemoveCommentThreadTargetOnCommentThreadMutation({ + * variables: { + * commentThreadId: // value for 'commentThreadId' + * commentThreadTargetId: // value for 'commentThreadTargetId' + * }, + * }); + */ +export function useRemoveCommentThreadTargetOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveCommentThreadTargetOnCommentThreadDocument, options); + } +export type RemoveCommentThreadTargetOnCommentThreadMutationHookResult = ReturnType; +export type RemoveCommentThreadTargetOnCommentThreadMutationResult = Apollo.MutationResult; +export type RemoveCommentThreadTargetOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; export const GetCompaniesDocument = gql` query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) { companies: findManyCompany(orderBy: $orderBy, where: $where) { @@ -2085,8 +2548,8 @@ export type EmptyQueryQueryHookResult = ReturnType; export type EmptyQueryLazyQueryHookResult = ReturnType; export type EmptyQueryQueryResult = Apollo.QueryResult; export const SearchCompanyQueryDocument = gql` - query SearchCompanyQuery($where: CompanyWhereInput, $limit: Int) { - searchResults: findManyCompany(where: $where, take: $limit) { + query SearchCompanyQuery($where: CompanyWhereInput, $limit: Int, $orderBy: [CompanyOrderByWithRelationInput!]) { + searchResults: findManyCompany(where: $where, take: $limit, orderBy: $orderBy) { id name domainName @@ -2108,6 +2571,7 @@ export const SearchCompanyQueryDocument = gql` * variables: { * where: // value for 'where' * limit: // value for 'limit' + * orderBy: // value for 'orderBy' * }, * }); */ diff --git a/front/src/modules/comments/components/comments/CommentThreadRelationPicker.tsx b/front/src/modules/comments/components/comments/CommentThreadRelationPicker.tsx index 5de2a9893..146ac337a 100644 --- a/front/src/modules/comments/components/comments/CommentThreadRelationPicker.tsx +++ b/front/src/modules/comments/components/comments/CommentThreadRelationPicker.tsx @@ -1,15 +1,16 @@ import { useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { autoUpdate, flip, offset, - shift, size, useFloating, } from '@floating-ui/react'; import { debounce } from 'lodash'; +import { v4 } from 'uuid'; import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer'; import CompanyChip from '@/companies/components/CompanyChip'; @@ -19,17 +20,25 @@ import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem'; import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer'; import { DropdownMenuSearch } from '@/ui/components/menu/DropdownMenuSearch'; import { DropdownMenuSeparator } from '@/ui/components/menu/DropdownMenuSeparator'; +import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef'; import { IconArrowUpRight } from '@/ui/icons'; import { Avatar } from '@/users/components/Avatar'; import { getLogoUrlFromDomainName } from '@/utils/utils'; -import { QueryMode, useSearchCompanyQueryQuery } from '~/generated/graphql'; +import { + CommentableType, + QueryMode, + SortOrder, + useAddCommentThreadTargetOnCommentThreadMutation, + useRemoveCommentThreadTargetOnCommentThreadMutation, + useSearchCompanyQueryQuery, +} from '~/generated/graphql'; type OwnProps = { commentThread: CommentThreadForDrawer; }; const StyledContainer = styled.div` - align-items: center; + align-items: flex-start; display: flex; flex-direction: row; gap: ${(props) => props.theme.spacing(2)}; @@ -38,10 +47,23 @@ const StyledContainer = styled.div` width: 100%; `; +const StyledLabelContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + + gap: ${(props) => props.theme.spacing(2)}; + + padding-bottom: ${(props) => props.theme.spacing(2)}; + padding-top: ${(props) => props.theme.spacing(2)}; +`; + const StyledRelationLabel = styled.div` color: ${(props) => props.theme.text60}; display: flex; flex-direction: row; + + user-select: none; `; const StyledRelationContainer = styled.div` @@ -53,43 +75,23 @@ const StyledRelationContainer = styled.div` cursor: pointer; display: flex; - gap: ${(props) => props.theme.spacing(2)}; + flex-wrap: wrap; - height: calc(32px - 2 * var(--vertical-padding)); + gap: ${(props) => props.theme.spacing(2)}; &:hover { background-color: ${(props) => props.theme.secondaryBackground}; border: 1px solid ${(props) => props.theme.lightBorder}; } + min-height: calc(32px - 2 * var(--vertical-padding)); + overflow: hidden; padding: var(--vertical-padding) var(--horizontal-padding); - width: calc(100% - 2 * var(--horizontal-padding)); `; -// TODO: refactor icon button with new figma and merge -// const StyledAddButton = styled.div` -// align-items: center; -// background: ${(props) => props.theme.primaryBackgroundTransparent}; -// border-radius: ${(props) => props.theme.borderRadius}; -// box-shadow: ${(props) => props.theme.modalBoxShadow}; - -// cursor: pointer; -// display: flex; -// flex-direction: row; - -// &:hover { -// background-color: ${(props) => props.theme.tertiaryBackground}; -// } - -// height: 20px; -// justify-content: center; - -// width: 20px; -// `; - export function CommentThreadRelationPicker({ commentThread }: OwnProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const [searchFilter, setSearchFilter] = useState(''); @@ -98,11 +100,33 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) { leading: true, }); + function exitEditMode() { + setIsMenuOpen(false); + setSearchFilter(''); + } + + useHotkeys( + ['esc', 'enter'], + () => { + exitEditMode(); + }, + { + enableOnContentEditable: true, + enableOnFormTags: true, + }, + [exitEditMode], + ); + const { refs, floatingStyles } = useFloating({ - strategy: 'fixed', - middleware: [offset(), flip(), shift(), size()], + strategy: 'absolute', + middleware: [offset(), flip(), size()], whileElementsMounted: autoUpdate, open: isMenuOpen, + placement: 'bottom-start', + }); + + useListenClickOutsideArrayOfRef([refs.floating, refs.domReference], () => { + exitEditMode(); }); const theme = useTheme(); @@ -111,29 +135,63 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) { ?.filter((relation) => relation.commentableType === 'Company') .map((relation) => relation.commentableId); - // const personIds = commentThread.commentThreadTargets - // ?.filter((relation) => relation.commentableType === 'Person') - // .map((relation) => relation.commentableId); - - const { data: dataForChips } = useSearchCompanyQueryQuery({ + const { data: selectedCompaniesData } = useSearchCompanyQueryQuery({ variables: { where: { id: { in: companyIds, }, }, + orderBy: { + name: SortOrder.Asc, + }, }, }); - const { data: dataForSelect } = useSearchCompanyQueryQuery({ + const { data: filteredSelectedCompaniesData } = useSearchCompanyQueryQuery({ variables: { where: { - name: { - contains: `%${searchFilter}%`, - mode: QueryMode.Insensitive, - }, + AND: [ + { + name: { + contains: `%${searchFilter}%`, + mode: QueryMode.Insensitive, + }, + }, + { + id: { + in: companyIds, + }, + }, + ], + }, + orderBy: { + name: SortOrder.Asc, + }, + }, + }); + + const { data: companiesToSelectData } = useSearchCompanyQueryQuery({ + variables: { + where: { + AND: [ + { + name: { + contains: `%${searchFilter}%`, + mode: QueryMode.Insensitive, + }, + }, + { + id: { + notIn: companyIds, + }, + }, + ], + }, + limit: 10, + orderBy: { + name: SortOrder.Asc, }, - limit: 5, }, }); @@ -145,18 +203,65 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) { setIsMenuOpen((isOpen) => !isOpen); } - const companiesForChips = dataForChips?.searchResults ?? []; - const companiesForSelect = dataForSelect?.searchResults ?? []; + const [addCommentThreadTargetOnCommentThread] = + useAddCommentThreadTargetOnCommentThreadMutation({ + refetchQueries: ['GetCompanies'], + }); + + const [removeCommentThreadTargetOnCommentThread] = + useRemoveCommentThreadTargetOnCommentThreadMutation({ + refetchQueries: ['GetCompanies'], + }); + + function handleCheckItemChange(newCheckedValue: boolean, itemId: string) { + if (newCheckedValue) { + addCommentThreadTargetOnCommentThread({ + variables: { + commentableEntityId: itemId, + commentableEntityType: CommentableType.Company, + commentThreadId: commentThread.id, + commentThreadTargetCreationDate: new Date().toISOString(), + commentThreadTargetId: v4(), + }, + }); + } else { + const foundCorrespondingTarget = commentThread.commentThreadTargets?.find( + (target) => target.commentableId === itemId, + ); + + if (foundCorrespondingTarget) { + removeCommentThreadTargetOnCommentThread({ + variables: { + commentThreadId: commentThread.id, + commentThreadTargetId: foundCorrespondingTarget.id, + }, + }); + } + } + } + + const selectedCompanies = selectedCompaniesData?.searchResults ?? []; + + const filteredSelectedCompanies = + filteredSelectedCompaniesData?.searchResults ?? []; + const companiesToSelect = companiesToSelectData?.searchResults ?? []; + + const companiesInDropdown = [ + ...filteredSelectedCompanies, + ...companiesToSelect, + ]; return ( - - Relations + + + Relations + - {companiesForChips?.map((company) => ( + {selectedCompanies?.map((company) => ( ))} - {/* - - */} {isMenuOpen && ( - {companiesForSelect?.slice(0, 5)?.map((company) => ( + {companiesInDropdown?.map((company) => ( companyForChip.id) + selectedCompanies + ?.map((selectedCompany) => selectedCompany.id) ?.includes(company.id) ?? false } - onChange={(newCheckedValue) => { - if (newCheckedValue) { - } - }} + onChange={(newCheckedValue) => + handleCheckItemChange(newCheckedValue, company.id) + } > ))} - {companiesForSelect?.length === 0 && ( + {companiesInDropdown?.length === 0 && ( No result )} diff --git a/front/src/modules/comments/services/select.ts b/front/src/modules/comments/services/select.ts index d914fd0ed..64bee0d3a 100644 --- a/front/src/modules/comments/services/select.ts +++ b/front/src/modules/comments/services/select.ts @@ -26,6 +26,7 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql` } } commentThreadTargets { + id commentableId commentableType } diff --git a/front/src/modules/comments/services/update.ts b/front/src/modules/comments/services/update.ts new file mode 100644 index 000000000..4e10028a7 --- /dev/null +++ b/front/src/modules/comments/services/update.ts @@ -0,0 +1,62 @@ +import { gql } from '@apollo/client'; + +export const ADD_COMMENT_THREAD_TARGET = gql` + mutation AddCommentThreadTargetOnCommentThread( + $commentThreadId: String! + $commentThreadTargetCreationDate: DateTime! + $commentThreadTargetId: String! + $commentableEntityId: String! + $commentableEntityType: CommentableType! + ) { + updateOneCommentThread( + where: { id: $commentThreadId } + data: { + commentThreadTargets: { + connectOrCreate: { + create: { + id: $commentThreadTargetId + createdAt: $commentThreadTargetCreationDate + commentableType: $commentableEntityType + commentableId: $commentableEntityId + } + where: { id: $commentThreadTargetId } + } + } + } + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentableType + commentableId + } + } + } +`; + +export const REMOVE_COMMENT_THREAD_TARGET = gql` + mutation RemoveCommentThreadTargetOnCommentThread( + $commentThreadId: String! + $commentThreadTargetId: String! + ) { + updateOneCommentThread( + where: { id: $commentThreadId } + data: { commentThreadTargets: { delete: { id: $commentThreadTargetId } } } + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentableType + commentableId + } + } + } +`; diff --git a/front/src/modules/companies/components/CompanyChip.tsx b/front/src/modules/companies/components/CompanyChip.tsx index d4618e881..fc8ff3a6a 100644 --- a/front/src/modules/companies/components/CompanyChip.tsx +++ b/front/src/modules/companies/components/CompanyChip.tsx @@ -14,6 +14,8 @@ const StyledContainer = styled.span` color: ${(props) => props.theme.text80}; display: inline-flex; gap: ${(props) => props.theme.spacing(1)}; + height: calc(20px - 2 * ${(props) => props.theme.spacing(1)}); + padding: ${(props) => props.theme.spacing(1)}; user-select: none; diff --git a/front/src/modules/search/services/search.ts b/front/src/modules/search/services/search.ts index fa9af2c5a..7ef9651a5 100644 --- a/front/src/modules/search/services/search.ts +++ b/front/src/modules/search/services/search.ts @@ -39,8 +39,16 @@ export const EMPTY_QUERY = gql` `; export const SEARCH_COMPANY_QUERY = gql` - query SearchCompanyQuery($where: CompanyWhereInput, $limit: Int) { - searchResults: findManyCompany(where: $where, take: $limit) { + query SearchCompanyQuery( + $where: CompanyWhereInput + $limit: Int + $orderBy: [CompanyOrderByWithRelationInput!] + ) { + searchResults: findManyCompany( + where: $where + take: $limit + orderBy: $orderBy + ) { id name domainName diff --git a/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx b/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx index 1ef52b5a1..c923c868c 100644 --- a/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx +++ b/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx @@ -9,6 +9,9 @@ export const DropdownMenuItemContainer = styled.div` flex-direction: column; gap: 2px; height: 100%; + max-height: 180px; + overflow-y: auto; + padding: var(--padding); width: calc(100% - 2 * var(--padding)); `; diff --git a/front/src/modules/ui/hooks/useListenClickOutsideArrayOfRef.ts b/front/src/modules/ui/hooks/useListenClickOutsideArrayOfRef.ts index 07c65cacf..2d3c4a467 100644 --- a/front/src/modules/ui/hooks/useListenClickOutsideArrayOfRef.ts +++ b/front/src/modules/ui/hooks/useListenClickOutsideArrayOfRef.ts @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { isDefined } from '@/utils/type-guards/isDefined'; -export function useListenClickOutsideArrayOfRef( +export function useListenClickOutsideArrayOfRef( arrayOfRef: Array>, outsideClickCallback: (event?: MouseEvent | TouchEvent) => void, ) { diff --git a/server/src/api/resolvers/comment-thread.resolver.ts b/server/src/api/resolvers/comment-thread.resolver.ts index aa3ee2dd3..aa58dc572 100644 --- a/server/src/api/resolvers/comment-thread.resolver.ts +++ b/server/src/api/resolvers/comment-thread.resolver.ts @@ -9,6 +9,7 @@ import { CreateOneCommentThreadArgs } from '../@generated/comment-thread/create- import { CreateOneCommentThreadGuard } from './guards/create-one-comment-thread.guard'; import { FindManyCommentThreadArgs } from '../@generated/comment-thread/find-many-comment-thread.args'; import { ArgsService } from './services/args.service'; +import { UpdateOneCommentThreadArgs } from '../@generated/comment-thread/update-one-comment-thread.args'; @UseGuards(JwtAuthGuard) @Resolver(() => CommentThread) @@ -67,6 +68,20 @@ export class CommentThreadResolver { return createdCommentThread; } + @Mutation(() => CommentThread, { + nullable: false, + }) + async updateOneCommentThread( + @Args() args: UpdateOneCommentThreadArgs, + ): Promise { + const updatedCommentThread = await this.prismaService.commentThread.update({ + data: args.data, + where: args.where, + }); + + return updatedCommentThread; + } + @Query(() => [CommentThread]) async findManyCommentThreads( @Args() args: FindManyCommentThreadArgs,