From a6b2fd75ba653715bef1d6469daa8f9fd17a7fe1 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 27 Jun 2023 09:00:14 -0700 Subject: [PATCH] Enable comment deletion on CommentDrawer (#460) * Enable comment deletion on people and companies page * Add storybook test --- front/src/generated/graphql.tsx | 46 +++++++++++++ .../comments/components/CommentHeader.tsx | 66 ++++++++++--------- .../comments/components/CommentThread.tsx | 17 ++++- .../components/CommentThreadActionBar.tsx | 49 ++++++++++++++ .../comments/components/CommentThreadItem.tsx | 6 +- .../__stories__/CommentHeader.stories.tsx | 13 ++++ front/src/modules/comments/services/update.ts | 8 +++ server/src/ability/ability.factory.ts | 1 + .../resolvers/comment-thread.resolver.ts | 16 +++++ .../migration.sql | 5 ++ .../migration.sql | 5 ++ server/src/database/schema.prisma | 4 +- 12 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 front/src/modules/comments/components/CommentThreadActionBar.tsx create mode 100644 server/src/database/migrations/20230627132509_cascade_delete_comment/migration.sql create mode 100644 server/src/database/migrations/20230627132647_cascade_delete_comment_thread_target/migration.sql diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index ae25ddabd..e3b062db4 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -711,6 +711,7 @@ export type Mutation = { createOneCompany: Company; createOnePerson: Person; createOnePipelineProgress: PipelineProgress; + deleteManyCommentThreads: AffectedRows; deleteManyCompany: AffectedRows; deleteManyPerson: AffectedRows; deleteManyPipelineProgress: AffectedRows; @@ -754,6 +755,11 @@ export type MutationCreateOnePipelineProgressArgs = { }; +export type MutationDeleteManyCommentThreadsArgs = { + where?: InputMaybe; +}; + + export type MutationDeleteManyCompanyArgs = { where?: InputMaybe; }; @@ -1645,6 +1651,13 @@ export type RemoveCommentThreadTargetOnCommentThreadMutationVariables = Exact<{ 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 DeleteCommentThreadMutationVariables = Exact<{ + commentThreadId: Scalars['String']; +}>; + + +export type DeleteCommentThreadMutation = { __typename?: 'Mutation', deleteManyCommentThreads: { __typename?: 'AffectedRows', count: number } }; + export type GetCompaniesQueryVariables = Exact<{ orderBy?: InputMaybe | CompanyOrderByWithRelationInput>; where?: InputMaybe; @@ -2240,6 +2253,39 @@ export function useRemoveCommentThreadTargetOnCommentThreadMutation(baseOptions? export type RemoveCommentThreadTargetOnCommentThreadMutationHookResult = ReturnType; export type RemoveCommentThreadTargetOnCommentThreadMutationResult = Apollo.MutationResult; export type RemoveCommentThreadTargetOnCommentThreadMutationOptions = Apollo.BaseMutationOptions; +export const DeleteCommentThreadDocument = gql` + mutation DeleteCommentThread($commentThreadId: String!) { + deleteManyCommentThreads(where: {id: {equals: $commentThreadId}}) { + count + } +} + `; +export type DeleteCommentThreadMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteCommentThreadMutation__ + * + * To run a mutation, you first call `useDeleteCommentThreadMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteCommentThreadMutation` 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 [deleteCommentThreadMutation, { data, loading, error }] = useDeleteCommentThreadMutation({ + * variables: { + * commentThreadId: // value for 'commentThreadId' + * }, + * }); + */ +export function useDeleteCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteCommentThreadDocument, options); + } +export type DeleteCommentThreadMutationHookResult = ReturnType; +export type DeleteCommentThreadMutationResult = Apollo.MutationResult; +export type DeleteCommentThreadMutationOptions = Apollo.BaseMutationOptions; export const GetCompaniesDocument = gql` query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) { companies: findManyCompany(orderBy: $orderBy, where: $where) { diff --git a/front/src/modules/comments/components/CommentHeader.tsx b/front/src/modules/comments/components/CommentHeader.tsx index bdbfba74b..3de95b834 100644 --- a/front/src/modules/comments/components/CommentHeader.tsx +++ b/front/src/modules/comments/components/CommentHeader.tsx @@ -12,25 +12,29 @@ import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString'; type OwnProps = { comment: Pick; + actionBar?: React.ReactNode; }; const StyledContainer = styled.div` align-items: center; display: flex; - flex-direction: row; - - gap: ${({ theme }) => theme.spacing(1)}; - - justify-content: flex-start; + justify-content: space-between; padding: ${({ theme }) => theme.spacing(1)}; + width: calc(100% - ${({ theme }) => theme.spacing(1)}); +`; + +const StyledLeftContainer = styled.div` + align-items: end; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; `; const StyledName = styled.div` color: ${({ theme }) => theme.font.color.primary}; - font-size: 13px; - font-weight: 400; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.regular}; max-width: 160px; overflow: hidden; text-overflow: ellipsis; @@ -39,12 +43,9 @@ const StyledName = styled.div` const StyledDate = styled.div` color: ${({ theme }) => theme.font.color.light}; - font-size: 12px; - font-weight: 400; - + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.regular}; margin-left: ${({ theme }) => theme.spacing(1)}; - - padding-top: 1.5px; `; const StyledTooltip = styled(Tooltip)` @@ -62,7 +63,7 @@ const StyledTooltip = styled(Tooltip)` padding: 8px; `; -export function CommentHeader({ comment }: OwnProps) { +export function CommentHeader({ comment, actionBar }: OwnProps) { const theme = useTheme(); const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt); const exactCreatedAt = beautifyExactDate(comment.createdAt); @@ -79,23 +80,28 @@ export function CommentHeader({ comment }: OwnProps) { return ( - - {authorName} - {showDate && ( - <> - {beautifiedCreatedAt} - - - )} + + + {authorName} + {showDate && ( + <> + + {beautifiedCreatedAt} + + + + )} + +
{actionBar}
); } diff --git a/front/src/modules/comments/components/CommentThread.tsx b/front/src/modules/comments/components/CommentThread.tsx index 1a0de664b..adc02a01f 100644 --- a/front/src/modules/comments/components/CommentThread.tsx +++ b/front/src/modules/comments/components/CommentThread.tsx @@ -15,6 +15,7 @@ import { useCreateCommentMutation } from '~/generated/graphql'; import { GET_COMMENT_THREADS_BY_TARGETS } from '../services'; +import { CommentThreadActionBar } from './CommentThreadActionBar'; import { CommentThreadItem } from './CommentThreadItem'; import { CommentThreadRelationPicker } from './CommentThreadRelationPicker'; @@ -38,7 +39,7 @@ const StyledThreadItemListContainer = styled.div` align-items: flex-start; display: flex; - flex-direction: column-reverse; + flex-direction: column; gap: ${({ theme }) => theme.spacing(4)}; justify-content: flex-start; @@ -86,8 +87,18 @@ export function CommentThread({ commentThread }: OwnProps) { return ( - {commentThread.comments?.map((comment) => ( - + {commentThread.comments?.map((comment, index) => ( + + ) : ( + <> + ) + } + /> ))} diff --git a/front/src/modules/comments/components/CommentThreadActionBar.tsx b/front/src/modules/comments/components/CommentThreadActionBar.tsx new file mode 100644 index 000000000..5c2daf672 --- /dev/null +++ b/front/src/modules/comments/components/CommentThreadActionBar.tsx @@ -0,0 +1,49 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; + +import { GET_COMPANIES } from '@/companies/services'; +import { GET_PEOPLE } from '@/people/services'; +import { IconTrash } from '@/ui/icons'; +import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; +import { useDeleteCommentThreadMutation } from '~/generated/graphql'; + +import { GET_COMMENT_THREADS_BY_TARGETS } from '../services'; + +const StyledContainer = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + cursor: pointer; +`; + +type OwnProps = { + commentThreadId: string; +}; + +export function CommentThreadActionBar({ commentThreadId }: OwnProps) { + const theme = useTheme(); + const [createCommentMutation] = useDeleteCommentThreadMutation(); + const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); + + function deleteCommentThread() { + createCommentMutation({ + variables: { commentThreadId }, + refetchQueries: [ + getOperationName(GET_COMPANIES) ?? '', + getOperationName(GET_PEOPLE) ?? '', + getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '', + ], + }); + setIsRightDrawerOpen(false); + } + + return ( + + + + ); +} diff --git a/front/src/modules/comments/components/CommentThreadItem.tsx b/front/src/modules/comments/components/CommentThreadItem.tsx index f4c40d27f..a3c81ebba 100644 --- a/front/src/modules/comments/components/CommentThreadItem.tsx +++ b/front/src/modules/comments/components/CommentThreadItem.tsx @@ -6,6 +6,7 @@ import { CommentHeader } from './CommentHeader'; type OwnProps = { comment: CommentForDrawer; + actionBar?: React.ReactNode; }; const StyledContainer = styled.div` @@ -14,6 +15,7 @@ const StyledContainer = styled.div` flex-direction: column; gap: ${({ theme }) => theme.spacing(1)}; justify-content: flex-start; + width: 100%; `; const StyledCommentBody = styled.div` @@ -28,10 +30,10 @@ const StyledCommentBody = styled.div` text-align: left; `; -export function CommentThreadItem({ comment }: OwnProps) { +export function CommentThreadItem({ comment, actionBar }: OwnProps) { return ( - + {comment.body} ); diff --git a/front/src/modules/comments/components/__stories__/CommentHeader.stories.tsx b/front/src/modules/comments/components/__stories__/CommentHeader.stories.tsx index 37a1cfdd7..238ac4c1f 100644 --- a/front/src/modules/comments/components/__stories__/CommentHeader.stories.tsx +++ b/front/src/modules/comments/components/__stories__/CommentHeader.stories.tsx @@ -7,6 +7,7 @@ import { mockedUsersData } from '~/testing/mock-data/users'; import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; import { CommentHeader } from '../CommentHeader'; +import { CommentThreadActionBar } from '../CommentThreadActionBar'; const meta: Meta = { title: 'Modules/Comments/CommentHeader', @@ -114,3 +115,15 @@ export const WithLongUserName: Story = { />, ), }; + +export const WithActionBar: Story = { + render: getRenderWrapperForComponent( + } + />, + ), +}; diff --git a/front/src/modules/comments/services/update.ts b/front/src/modules/comments/services/update.ts index 4e10028a7..6cee16a11 100644 --- a/front/src/modules/comments/services/update.ts +++ b/front/src/modules/comments/services/update.ts @@ -60,3 +60,11 @@ export const REMOVE_COMMENT_THREAD_TARGET = gql` } } `; + +export const DELETE_COMMENT_THREAD = gql` + mutation DeleteCommentThread($commentThreadId: String!) { + deleteManyCommentThreads(where: { id: { equals: $commentThreadId } }) { + count + } + } +`; diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index 63c8be5e6..cc3f55236 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -78,6 +78,7 @@ export class AbilityFactory { can(AbilityAction.Read, 'CommentThread', { workspaceId: workspace.id }); can(AbilityAction.Create, 'CommentThread'); can(AbilityAction.Update, 'CommentThread', { workspaceId: workspace.id }); + can(AbilityAction.Delete, 'CommentThread', { workspaceId: workspace.id }); // Comment can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id }); diff --git a/server/src/core/comment/resolvers/comment-thread.resolver.ts b/server/src/core/comment/resolvers/comment-thread.resolver.ts index 764ea34f2..53b982eb1 100644 --- a/server/src/core/comment/resolvers/comment-thread.resolver.ts +++ b/server/src/core/comment/resolvers/comment-thread.resolver.ts @@ -18,12 +18,15 @@ import { AbilityGuard } from 'src/guards/ability.guard'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; import { CreateCommentThreadAbilityHandler, + DeleteCommentThreadAbilityHandler, ReadCommentThreadAbilityHandler, UpdateCommentThreadAbilityHandler, } from 'src/ability/handlers/comment-thread.ability-handler'; import { UserAbility } from 'src/decorators/user-ability.decorator'; import { AppAbility } from 'src/ability/ability.factory'; import { accessibleBy } from '@casl/prisma'; +import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; +import { DeleteManyCommentThreadArgs } from 'src/core/@generated/comment-thread/delete-many-comment-thread.args'; @UseGuards(JwtAuthGuard) @Resolver(() => CommentThread) @@ -99,4 +102,17 @@ export class CommentThreadResolver { return result; } + + @Mutation(() => AffectedRows, { + nullable: false, + }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteCommentThreadAbilityHandler) + async deleteManyCommentThreads( + @Args() args: DeleteManyCommentThreadArgs, + ): Promise { + return this.commentThreadService.deleteMany({ + ...args, + }); + } } diff --git a/server/src/database/migrations/20230627132509_cascade_delete_comment/migration.sql b/server/src/database/migrations/20230627132509_cascade_delete_comment/migration.sql new file mode 100644 index 000000000..116ebd672 --- /dev/null +++ b/server/src/database/migrations/20230627132509_cascade_delete_comment/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "comments" DROP CONSTRAINT "comments_commentThreadId_fkey"; + +-- AddForeignKey +ALTER TABLE "comments" ADD CONSTRAINT "comments_commentThreadId_fkey" FOREIGN KEY ("commentThreadId") REFERENCES "comment_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/src/database/migrations/20230627132647_cascade_delete_comment_thread_target/migration.sql b/server/src/database/migrations/20230627132647_cascade_delete_comment_thread_target/migration.sql new file mode 100644 index 000000000..67a3839e6 --- /dev/null +++ b/server/src/database/migrations/20230627132647_cascade_delete_comment_thread_target/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "comment_thread_targets" DROP CONSTRAINT "comment_thread_targets_commentThreadId_fkey"; + +-- AddForeignKey +ALTER TABLE "comment_thread_targets" ADD CONSTRAINT "comment_thread_targets_commentThreadId_fkey" FOREIGN KEY ("commentThreadId") REFERENCES "comment_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma index 38978f66f..4a2005af4 100644 --- a/server/src/database/schema.prisma +++ b/server/src/database/schema.prisma @@ -255,7 +255,7 @@ model Comment { authorId String author User @relation(fields: [authorId], references: [id]) commentThreadId String - commentThread CommentThread @relation(fields: [commentThreadId], references: [id]) + commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade) /// @TypeGraphQL.omit(input: true, output: true) workspaceId String /// @TypeGraphQL.omit(input: true, output: true) @@ -275,7 +275,7 @@ model CommentThreadTarget { updatedAt DateTime @updatedAt deletedAt DateTime? commentThreadId String - commentThread CommentThread @relation(fields: [commentThreadId], references: [id]) + commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade) commentableType CommentableType commentableId String