diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 19cd17378..819703987 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1915,24 +1915,21 @@ export type GetCommentThreadQueryVariables = Exact<{ export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, body?: string | null, title?: string | null, type: ActivityType, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, commentableId: string, commentableType: CommentableType }> | null }> }; -export type AddCommentThreadTargetOnCommentThreadMutationVariables = Exact<{ +export type AddCommentThreadTargetsOnCommentThreadMutationVariables = Exact<{ commentThreadId: Scalars['String']; - commentThreadTargetCreationDate: Scalars['DateTime']; - commentThreadTargetId: Scalars['String']; - commentableEntityId: Scalars['String']; - commentableEntityType: CommentableType; + commentThreadTargetInputs: Array | CommentThreadTargetCreateManyCommentThreadInput; }>; -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 AddCommentThreadTargetsOnCommentThreadMutation = { __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<{ +export type RemoveCommentThreadTargetsOnCommentThreadMutationVariables = Exact<{ commentThreadId: Scalars['String']; - commentThreadTargetId: Scalars['String']; + commentThreadTargetIds: Array | 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 RemoveCommentThreadTargetsOnCommentThreadMutation = { __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 DeleteCommentThreadMutationVariables = Exact<{ commentThreadId: Scalars['String']; @@ -2531,11 +2528,11 @@ 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!) { +export const AddCommentThreadTargetsOnCommentThreadDocument = gql` + mutation AddCommentThreadTargetsOnCommentThread($commentThreadId: String!, $commentThreadTargetInputs: [CommentThreadTargetCreateManyCommentThreadInput!]!) { updateOneCommentThread( where: {id: $commentThreadId} - data: {commentThreadTargets: {connectOrCreate: {create: {id: $commentThreadTargetId, createdAt: $commentThreadTargetCreationDate, commentableType: $commentableEntityType, commentableId: $commentableEntityId}, where: {id: $commentThreadTargetId}}}} + data: {commentThreadTargets: {createMany: {data: $commentThreadTargetInputs}}} ) { id createdAt @@ -2550,41 +2547,38 @@ export const AddCommentThreadTargetOnCommentThreadDocument = gql` } } `; -export type AddCommentThreadTargetOnCommentThreadMutationFn = Apollo.MutationFunction; +export type AddCommentThreadTargetsOnCommentThreadMutationFn = Apollo.MutationFunction; /** - * __useAddCommentThreadTargetOnCommentThreadMutation__ + * __useAddCommentThreadTargetsOnCommentThreadMutation__ * - * 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: + * To run a mutation, you first call `useAddCommentThreadTargetsOnCommentThreadMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddCommentThreadTargetsOnCommentThreadMutation` 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({ + * const [addCommentThreadTargetsOnCommentThreadMutation, { data, loading, error }] = useAddCommentThreadTargetsOnCommentThreadMutation({ * variables: { * commentThreadId: // value for 'commentThreadId' - * commentThreadTargetCreationDate: // value for 'commentThreadTargetCreationDate' - * commentThreadTargetId: // value for 'commentThreadTargetId' - * commentableEntityId: // value for 'commentableEntityId' - * commentableEntityType: // value for 'commentableEntityType' + * commentThreadTargetInputs: // value for 'commentThreadTargetInputs' * }, * }); */ -export function useAddCommentThreadTargetOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useAddCommentThreadTargetsOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(AddCommentThreadTargetOnCommentThreadDocument, options); + return Apollo.useMutation(AddCommentThreadTargetsOnCommentThreadDocument, 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!) { +export type AddCommentThreadTargetsOnCommentThreadMutationHookResult = ReturnType; +export type AddCommentThreadTargetsOnCommentThreadMutationResult = Apollo.MutationResult; +export type AddCommentThreadTargetsOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; +export const RemoveCommentThreadTargetsOnCommentThreadDocument = gql` + mutation RemoveCommentThreadTargetsOnCommentThread($commentThreadId: String!, $commentThreadTargetIds: [String!]!) { updateOneCommentThread( where: {id: $commentThreadId} - data: {commentThreadTargets: {delete: {id: $commentThreadTargetId}}} + data: {commentThreadTargets: {deleteMany: {id: {in: $commentThreadTargetIds}}}} ) { id createdAt @@ -2599,33 +2593,33 @@ export const RemoveCommentThreadTargetOnCommentThreadDocument = gql` } } `; -export type RemoveCommentThreadTargetOnCommentThreadMutationFn = Apollo.MutationFunction; +export type RemoveCommentThreadTargetsOnCommentThreadMutationFn = Apollo.MutationFunction; /** - * __useRemoveCommentThreadTargetOnCommentThreadMutation__ + * __useRemoveCommentThreadTargetsOnCommentThreadMutation__ * - * 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: + * To run a mutation, you first call `useRemoveCommentThreadTargetsOnCommentThreadMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveCommentThreadTargetsOnCommentThreadMutation` 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({ + * const [removeCommentThreadTargetsOnCommentThreadMutation, { data, loading, error }] = useRemoveCommentThreadTargetsOnCommentThreadMutation({ * variables: { * commentThreadId: // value for 'commentThreadId' - * commentThreadTargetId: // value for 'commentThreadTargetId' + * commentThreadTargetIds: // value for 'commentThreadTargetIds' * }, * }); */ -export function useRemoveCommentThreadTargetOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useRemoveCommentThreadTargetsOnCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(RemoveCommentThreadTargetOnCommentThreadDocument, options); + return Apollo.useMutation(RemoveCommentThreadTargetsOnCommentThreadDocument, options); } -export type RemoveCommentThreadTargetOnCommentThreadMutationHookResult = ReturnType; -export type RemoveCommentThreadTargetOnCommentThreadMutationResult = Apollo.MutationResult; -export type RemoveCommentThreadTargetOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; +export type RemoveCommentThreadTargetsOnCommentThreadMutationHookResult = ReturnType; +export type RemoveCommentThreadTargetsOnCommentThreadMutationResult = Apollo.MutationResult; +export type RemoveCommentThreadTargetsOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; export const DeleteCommentThreadDocument = gql` mutation DeleteCommentThread($commentThreadId: String!) { deleteManyCommentThreads(where: {id: {equals: $commentThreadId}}) { diff --git a/front/src/modules/activities/components/CommentThreadRelationPicker.tsx b/front/src/modules/activities/components/CommentThreadRelationPicker.tsx index 92f2a8e97..e7222c109 100644 --- a/front/src/modules/activities/components/CommentThreadRelationPicker.tsx +++ b/front/src/modules/activities/components/CommentThreadRelationPicker.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import styled from '@emotion/styled'; import { autoUpdate, @@ -78,56 +78,98 @@ const StyledMenuWrapper = styled.div` export function CommentThreadRelationPicker({ commentThread }: OwnProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const [searchFilter, setSearchFilter] = useState(''); - - const peopleIds = - commentThread?.commentThreadTargets - ?.filter((relation) => relation.commentableType === 'Person') - .map((relation) => relation.commentableId) ?? []; - const companyIds = - commentThread?.commentThreadTargets - ?.filter((relation) => relation.commentableType === 'Company') - .map((relation) => relation.commentableId) ?? []; - - const personsForMultiSelect = useFilteredSearchPeopleQuery({ - searchFilter, - selectedIds: peopleIds, - }); - - const companiesForMultiSelect = useFilteredSearchCompanyQuery({ - searchFilter, - selectedIds: companyIds, - }); - + const [selectedEntityIds, setSelectedEntityIds] = useState< + Record + >({}); const { setHotkeyScopeAndMemorizePreviousScope, goBackToPreviousHotkeyScope, } = usePreviousHotkeyScope(); - function handleRelationContainerClick() { + const initialPeopleIds = useMemo( + () => + commentThread?.commentThreadTargets + ?.filter((relation) => relation.commentableType === 'Person') + .map((relation) => relation.commentableId) ?? [], + [commentThread?.commentThreadTargets], + ); + const initialCompanyIds = useMemo( + () => + commentThread?.commentThreadTargets + ?.filter((relation) => relation.commentableType === 'Company') + .map((relation) => relation.commentableId) ?? [], + [commentThread?.commentThreadTargets], + ); + + const initialSelectedEntityIds = useMemo( + () => + [...initialPeopleIds, ...initialCompanyIds].reduce< + Record + >((result, entityId) => ({ ...result, [entityId]: true }), {}), + [initialPeopleIds, initialCompanyIds], + ); + + const personsForMultiSelect = useFilteredSearchPeopleQuery({ + searchFilter, + selectedIds: initialPeopleIds, + }); + + const companiesForMultiSelect = useFilteredSearchCompanyQuery({ + searchFilter, + selectedIds: initialCompanyIds, + }); + + const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([ + personsForMultiSelect.selectedEntities, + companiesForMultiSelect.selectedEntities, + ]); + + const filteredSelectedEntities = + flatMapAndSortEntityForSelectArrayOfArrayByName([ + personsForMultiSelect.filteredSelectedEntities, + companiesForMultiSelect.filteredSelectedEntities, + ]); + + const entitiesToSelect = flatMapAndSortEntityForSelectArrayOfArrayByName([ + personsForMultiSelect.entitiesToSelect, + companiesForMultiSelect.entitiesToSelect, + ]); + + const handleCheckItemsChange = useHandleCheckableCommentThreadTargetChange({ + commentThread, + }); + + const exitEditMode = useCallback(() => { + goBackToPreviousHotkeyScope(); + setIsMenuOpen(false); + setSearchFilter(''); + + if (Object.values(selectedEntityIds).some((value) => !!value)) { + handleCheckItemsChange(selectedEntityIds, entitiesToSelect); + } + }, [ + entitiesToSelect, + selectedEntityIds, + goBackToPreviousHotkeyScope, + handleCheckItemsChange, + ]); + + const handleRelationContainerClick = useCallback(() => { if (isMenuOpen) { exitEditMode(); } else { setIsMenuOpen(true); + setSelectedEntityIds(initialSelectedEntityIds); setHotkeyScopeAndMemorizePreviousScope( RelationPickerHotkeyScope.RelationPicker, ); } - } - - // TODO: Place in a scoped recoil atom family - function handleFilterChange(newSearchFilter: string) { - setSearchFilter(newSearchFilter); - } - - const handleCheckItemChange = useHandleCheckableCommentThreadTargetChange({ - commentThread, - }); - - function exitEditMode() { - goBackToPreviousHotkeyScope(); - setIsMenuOpen(false); - setSearchFilter(''); - } + }, [ + initialSelectedEntityIds, + exitEditMode, + isMenuOpen, + setHotkeyScopeAndMemorizePreviousScope, + ]); useScopedHotkeys( ['esc', 'enter'], @@ -159,22 +201,6 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) { }, }); - const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.selectedEntities, - companiesForMultiSelect.selectedEntities, - ]); - - const filteredSelectedEntities = - flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.filteredSelectedEntities, - companiesForMultiSelect.filteredSelectedEntities, - ]); - - const entitiesToSelect = flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.entitiesToSelect, - companiesForMultiSelect.entitiesToSelect, - ]); - return ( diff --git a/front/src/modules/activities/hooks/useHandleCheckableCommentThreadTargetChange.ts b/front/src/modules/activities/hooks/useHandleCheckableCommentThreadTargetChange.ts index 25a4dc985..388477da4 100644 --- a/front/src/modules/activities/hooks/useHandleCheckableCommentThreadTargetChange.ts +++ b/front/src/modules/activities/hooks/useHandleCheckableCommentThreadTargetChange.ts @@ -6,8 +6,8 @@ import { GET_PEOPLE } from '@/people/queries'; import { CommentThread, CommentThreadTarget, - useAddCommentThreadTargetOnCommentThreadMutation, - useRemoveCommentThreadTargetOnCommentThreadMutation, + useAddCommentThreadTargetsOnCommentThreadMutation, + useRemoveCommentThreadTargetsOnCommentThreadMutation, } from '~/generated/graphql'; import { GET_COMMENT_THREADS_BY_TARGETS } from '../queries'; @@ -22,8 +22,8 @@ export function useHandleCheckableCommentThreadTargetChange({ >; }; }) { - const [addCommentThreadTargetOnCommentThread] = - useAddCommentThreadTargetOnCommentThreadMutation({ + const [addCommentThreadTargetsOnCommentThread] = + useAddCommentThreadTargetsOnCommentThreadMutation({ refetchQueries: [ getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_PEOPLE) ?? '', @@ -31,8 +31,8 @@ export function useHandleCheckableCommentThreadTargetChange({ ], }); - const [removeCommentThreadTargetOnCommentThread] = - useRemoveCommentThreadTargetOnCommentThreadMutation({ + const [removeCommentThreadTargetsOnCommentThread] = + useRemoveCommentThreadTargetsOnCommentThreadMutation({ refetchQueries: [ getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_PEOPLE) ?? '', @@ -40,36 +40,45 @@ export function useHandleCheckableCommentThreadTargetChange({ ], }); - return function handleCheckItemChange( - newCheckedValue: boolean, - entity: CommentableEntityForSelect, + return async function handleCheckItemsChange( + entityValues: Record, + entities: CommentableEntityForSelect[], ) { if (!commentThread) { return; } - if (newCheckedValue) { - addCommentThreadTargetOnCommentThread({ + + const currentEntityIds = commentThread.commentThreadTargets.map( + ({ commentableId }) => commentableId, + ); + + const entitiesToAdd = entities.filter( + ({ id }) => entityValues[id] && !currentEntityIds.includes(id), + ); + + if (entitiesToAdd.length) + await addCommentThreadTargetsOnCommentThread({ variables: { - commentableEntityId: entity.id, - commentableEntityType: entity.entityType, commentThreadId: commentThread.id, - commentThreadTargetCreationDate: new Date().toISOString(), - commentThreadTargetId: v4(), + commentThreadTargetInputs: entitiesToAdd.map((entity) => ({ + id: v4(), + createdAt: new Date().toISOString(), + commentableType: entity.entityType, + commentableId: entity.id, + })), }, }); - } else { - const foundCorrespondingTarget = commentThread.commentThreadTargets?.find( - (target) => target.commentableId === entity.id, - ); - if (foundCorrespondingTarget) { - removeCommentThreadTargetOnCommentThread({ - variables: { - commentThreadId: commentThread.id, - commentThreadTargetId: foundCorrespondingTarget.id, - }, - }); - } - } + const commentThreadTargetIdsToDelete = commentThread.commentThreadTargets + .filter(({ commentableId }) => !entityValues[commentableId]) + .map(({ id }) => id); + + if (commentThreadTargetIdsToDelete.length) + await removeCommentThreadTargetsOnCommentThread({ + variables: { + commentThreadId: commentThread.id, + commentThreadTargetIds: commentThreadTargetIdsToDelete, + }, + }); }; } diff --git a/front/src/modules/activities/queries/update.ts b/front/src/modules/activities/queries/update.ts index 673b47182..d3ad7d740 100644 --- a/front/src/modules/activities/queries/update.ts +++ b/front/src/modules/activities/queries/update.ts @@ -1,26 +1,15 @@ import { gql } from '@apollo/client'; -export const ADD_COMMENT_THREAD_TARGET = gql` - mutation AddCommentThreadTargetOnCommentThread( +export const ADD_COMMENT_THREAD_TARGETS = gql` + mutation AddCommentThreadTargetsOnCommentThread( $commentThreadId: String! - $commentThreadTargetCreationDate: DateTime! - $commentThreadTargetId: String! - $commentableEntityId: String! - $commentableEntityType: CommentableType! + $commentThreadTargetInputs: [CommentThreadTargetCreateManyCommentThreadInput!]! ) { updateOneCommentThread( where: { id: $commentThreadId } data: { commentThreadTargets: { - connectOrCreate: { - create: { - id: $commentThreadTargetId - createdAt: $commentThreadTargetCreationDate - commentableType: $commentableEntityType - commentableId: $commentableEntityId - } - where: { id: $commentThreadTargetId } - } + createMany: { data: $commentThreadTargetInputs } } } ) { @@ -38,14 +27,18 @@ export const ADD_COMMENT_THREAD_TARGET = gql` } `; -export const REMOVE_COMMENT_THREAD_TARGET = gql` - mutation RemoveCommentThreadTargetOnCommentThread( +export const REMOVE_COMMENT_THREAD_TARGETS = gql` + mutation RemoveCommentThreadTargetsOnCommentThread( $commentThreadId: String! - $commentThreadTargetId: String! + $commentThreadTargetIds: [String!]! ) { updateOneCommentThread( where: { id: $commentThreadId } - data: { commentThreadTargets: { delete: { id: $commentThreadTargetId } } } + data: { + commentThreadTargets: { + deleteMany: { id: { in: $commentThreadTargetIds } } + } + } ) { id createdAt diff --git a/front/src/modules/ui/input/components/Checkbox.tsx b/front/src/modules/ui/input/components/Checkbox.tsx index 91d10e91a..a79cf9e4c 100644 --- a/front/src/modules/ui/input/components/Checkbox.tsx +++ b/front/src/modules/ui/input/components/Checkbox.tsx @@ -79,7 +79,8 @@ export function Checkbox({ indeterminate, variant = CheckboxVariant.Primary, }: OwnProps) { - const [isInternalChecked, setIsInternalChecked] = React.useState(false); + const [isInternalChecked, setIsInternalChecked] = + React.useState(false); React.useEffect(() => { setIsInternalChecked(checked); diff --git a/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx b/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx index 6f473033c..93520ca68 100644 --- a/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx +++ b/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx @@ -23,17 +23,16 @@ export function MultipleEntitySelect< CustomEntityForSelect extends EntityForSelect, >({ entities, - onItemCheckChange, + onChange, onSearchFilterChange, searchFilter, + value, }: { entities: EntitiesForMultipleEntitySelect; searchFilter: string; onSearchFilterChange: (newSearchFilter: string) => void; - onItemCheckChange: ( - newCheckedValue: boolean, - entity: CustomEntityForSelect, - ) => void; + onChange: (value: Record) => void; + value: Record; }) { const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, { leading: true, @@ -61,13 +60,9 @@ export function MultipleEntitySelect< {entitiesInDropdown?.map((entity) => ( selectedEntity.id) - ?.includes(entity.id) ?? false - } + checked={value[entity.id]} onChange={(newCheckedValue) => - onItemCheckChange(newCheckedValue, entity) + onChange({ ...value, [entity.id]: newCheckedValue }) } >