Enable comment deletion on CommentDrawer (#460)
* Enable comment deletion on people and companies page * Add storybook test
This commit is contained in:
@ -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<CommentThreadWhereInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteManyCompanyArgs = {
|
||||
where?: InputMaybe<CompanyWhereInput>;
|
||||
};
|
||||
@ -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<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
||||
where?: InputMaybe<CompanyWhereInput>;
|
||||
@ -2240,6 +2253,39 @@ export function useRemoveCommentThreadTargetOnCommentThreadMutation(baseOptions?
|
||||
export type RemoveCommentThreadTargetOnCommentThreadMutationHookResult = ReturnType<typeof useRemoveCommentThreadTargetOnCommentThreadMutation>;
|
||||
export type RemoveCommentThreadTargetOnCommentThreadMutationResult = Apollo.MutationResult<RemoveCommentThreadTargetOnCommentThreadMutation>;
|
||||
export type RemoveCommentThreadTargetOnCommentThreadMutationOptions = Apollo.BaseMutationOptions<RemoveCommentThreadTargetOnCommentThreadMutation, RemoveCommentThreadTargetOnCommentThreadMutationVariables>;
|
||||
export const DeleteCommentThreadDocument = gql`
|
||||
mutation DeleteCommentThread($commentThreadId: String!) {
|
||||
deleteManyCommentThreads(where: {id: {equals: $commentThreadId}}) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteCommentThreadMutationFn = Apollo.MutationFunction<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>(DeleteCommentThreadDocument, options);
|
||||
}
|
||||
export type DeleteCommentThreadMutationHookResult = ReturnType<typeof useDeleteCommentThreadMutation>;
|
||||
export type DeleteCommentThreadMutationResult = Apollo.MutationResult<DeleteCommentThreadMutation>;
|
||||
export type DeleteCommentThreadMutationOptions = Apollo.BaseMutationOptions<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>;
|
||||
export const GetCompaniesDocument = gql`
|
||||
query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) {
|
||||
companies: findManyCompany(orderBy: $orderBy, where: $where) {
|
||||
|
||||
@ -12,25 +12,29 @@ import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
|
||||
type OwnProps = {
|
||||
comment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'>;
|
||||
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 (
|
||||
<StyledContainer>
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
size={theme.icon.size.md}
|
||||
placeholder={capitalizedFirstUsernameLetter}
|
||||
/>
|
||||
<StyledName>{authorName}</StyledName>
|
||||
{showDate && (
|
||||
<>
|
||||
<StyledDate id={`id-${commentId}`}>{beautifiedCreatedAt}</StyledDate>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#id-${commentId}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<StyledLeftContainer>
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
size={theme.icon.size.md}
|
||||
placeholder={capitalizedFirstUsernameLetter}
|
||||
/>
|
||||
<StyledName>{authorName}</StyledName>
|
||||
{showDate && (
|
||||
<>
|
||||
<StyledDate id={`id-${commentId}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledDate>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#id-${commentId}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledLeftContainer>
|
||||
<div>{actionBar}</div>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<StyledContainer>
|
||||
<StyledThreadItemListContainer>
|
||||
{commentThread.comments?.map((comment) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
{commentThread.comments?.map((comment, index) => (
|
||||
<CommentThreadItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
actionBar={
|
||||
index === 0 ? (
|
||||
<CommentThreadActionBar commentThreadId={commentThread.id} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
<CommentThreadRelationPicker commentThread={commentThread} />
|
||||
|
||||
@ -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 (
|
||||
<StyledContainer>
|
||||
<IconTrash
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.md}
|
||||
onClick={deleteCommentThread}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<StyledContainer>
|
||||
<CommentHeader comment={comment} />
|
||||
<CommentHeader comment={comment} actionBar={actionBar} />
|
||||
<StyledCommentBody>{comment.body}</StyledCommentBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -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<typeof CommentHeader> = {
|
||||
title: 'Modules/Comments/CommentHeader',
|
||||
@ -114,3 +115,15 @@ export const WithLongUserName: Story = {
|
||||
/>,
|
||||
),
|
||||
};
|
||||
|
||||
export const WithActionBar: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
|
||||
}}
|
||||
actionBar={<CommentThreadActionBar commentThreadId="test-id" />}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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<AffectedRows> {
|
||||
return this.commentThreadService.deleteMany({
|
||||
...args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user