mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 22:51:09 +10:00
switch to nx monorepo
This commit is contained in:
5
apps/client/src/features/comment/atoms/comment-atom.ts
Normal file
5
apps/client/src/features/comment/atoms/comment-atom.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const showCommentPopupAtom = atom<boolean>(false);
|
||||
export const activeCommentIdAtom = atom<string>('');
|
||||
export const draftCommentIdAtom = atom<string>('');
|
||||
@ -0,0 +1,16 @@
|
||||
import { Button, Group } from '@mantine/core';
|
||||
|
||||
type CommentActionsProps = {
|
||||
onSave: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
function CommentActions({ onSave, isLoading }: CommentActionsProps) {
|
||||
return (
|
||||
<Group justify="flex-end" pt={2} wrap="nowrap">
|
||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>Save</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentActions;
|
||||
@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Dialog, Group, Stack, Text } from '@mantine/core';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import {
|
||||
activeCommentIdAtom,
|
||||
draftCommentIdAtom,
|
||||
showCommentPopupAtom,
|
||||
} from '@/features/comment/atoms/comment-atom';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import CommentEditor from '@/features/comment/components/comment-editor';
|
||||
import CommentActions from '@/features/comment/components/comment-actions';
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
import { useCreateCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
import { asideStateAtom } from '@/components/navbar/atoms/sidebar-atom';
|
||||
|
||||
interface CommentDialogProps {
|
||||
editor: Editor,
|
||||
pageId: string,
|
||||
}
|
||||
|
||||
function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
const [comment, setComment] = useState('');
|
||||
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setAsideState] = useAtom(asideStateAtom);
|
||||
const useClickOutsideRef = useClickOutside(() => {
|
||||
handleDialogClose();
|
||||
});
|
||||
const createCommentMutation = useCreateCommentMutation();
|
||||
const { isPending } = createCommentMutation;
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setShowCommentPopup(false);
|
||||
editor.chain().focus().unsetCommentDecoration().run();
|
||||
};
|
||||
|
||||
const getSelectedText = () => {
|
||||
const { from, to } = editor.state.selection;
|
||||
return editor.state.doc.textBetween(from, to);
|
||||
};
|
||||
|
||||
const handleAddComment = async () => {
|
||||
try {
|
||||
const selectedText = getSelectedText();
|
||||
const commentData = {
|
||||
id: draftCommentId,
|
||||
pageId: pageId,
|
||||
content: JSON.stringify(comment),
|
||||
selection: selectedText,
|
||||
};
|
||||
|
||||
const createdComment = await createCommentMutation.mutateAsync(commentData);
|
||||
editor.chain().setComment(createdComment.id).unsetCommentDecoration().run();
|
||||
setActiveCommentId(createdComment.id);
|
||||
|
||||
setAsideState({ tab: 'comments', isAsideOpen: true });
|
||||
setTimeout(() => {
|
||||
const selector = `div[data-comment-id="${createdComment.id}"]`;
|
||||
const commentElement = document.querySelector(selector);
|
||||
commentElement?.scrollIntoView();
|
||||
});
|
||||
} finally {
|
||||
setShowCommentPopup(false);
|
||||
setDraftCommentId('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommentEditorChange = (newContent) => {
|
||||
setComment(newContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog opened={true} onClose={handleDialogClose} ref={useClickOutsideRef} size="lg" radius="md"
|
||||
w={300} position={{ bottom: 500, right: 50 }} withCloseButton withBorder>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Group>
|
||||
<Avatar size="sm" color="blue">{currentUser.user.name.charAt(0)}</Avatar>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" fw={500} lineClamp={1}>{currentUser.user.name}</Text>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<CommentEditor onUpdate={handleCommentEditorChange} placeholder="Write a comment"
|
||||
editable={true} autofocus={true}
|
||||
/>
|
||||
<CommentActions onSave={handleAddComment} isLoading={isPending}
|
||||
/>
|
||||
</Stack>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentDialog;
|
||||
@ -0,0 +1,58 @@
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import { Placeholder } from '@tiptap/extension-placeholder';
|
||||
import { Underline } from '@tiptap/extension-underline';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import { StarterKit } from '@tiptap/starter-kit';
|
||||
import classes from './comment.module.css';
|
||||
import { useFocusWithin } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
interface CommentEditorProps {
|
||||
defaultContent?: any;
|
||||
onUpdate?: any;
|
||||
editable: boolean;
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
}
|
||||
|
||||
const CommentEditor = forwardRef(({ defaultContent, onUpdate, editable, placeholder, autofocus }: CommentEditorProps,
|
||||
ref) => {
|
||||
const { ref: focusRef, focused } = useFocusWithin();
|
||||
|
||||
const commentEditor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
gapcursor: false,
|
||||
dropcursor: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder || 'Reply...',
|
||||
}),
|
||||
Underline,
|
||||
Link,
|
||||
],
|
||||
onUpdate({ editor }) {
|
||||
if (onUpdate) onUpdate(editor.getJSON());
|
||||
},
|
||||
content: defaultContent,
|
||||
editable,
|
||||
autofocus: (autofocus && 'end') || false,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
clearContent: () => {
|
||||
commentEditor.commands.clearContent();
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div ref={focusRef} className={classes.commentEditor}>
|
||||
<EditorContent editor={commentEditor}
|
||||
className={clsx(classes.ProseMirror, { [classes.focused]: focused })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default CommentEditor;
|
||||
@ -0,0 +1,110 @@
|
||||
import { Group, Text, Box } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
import classes from './comment.module.css';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { timeAgo } from '@/lib/time';
|
||||
import CommentEditor from '@/features/comment/components/comment-editor';
|
||||
import { pageEditorAtom } from '@/features/editor/atoms/editor-atoms';
|
||||
import CommentActions from '@/features/comment/components/comment-actions';
|
||||
import CommentMenu from '@/features/comment/components/comment-menu';
|
||||
import { useHover } from '@mantine/hooks';
|
||||
import { useDeleteCommentMutation, useUpdateCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
import { IComment } from '@/features/comment/types/comment.types';
|
||||
import { UserAvatar } from '@/components/ui/user-avatar';
|
||||
|
||||
interface CommentListItemProps {
|
||||
comment: IComment;
|
||||
}
|
||||
|
||||
function CommentListItem({ comment }: CommentListItemProps) {
|
||||
|
||||
const { hovered, ref } = useHover();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const editor = useAtomValue(pageEditorAtom);
|
||||
const [content, setContent] = useState(comment.content);
|
||||
const updateCommentMutation = useUpdateCommentMutation();
|
||||
const deleteCommentMutation = useDeleteCommentMutation(comment.pageId);
|
||||
|
||||
async function handleUpdateComment() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const commentToUpdate = {
|
||||
id: comment.id,
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
await updateCommentMutation.mutateAsync(commentToUpdate);
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to update comment:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteComment() {
|
||||
try {
|
||||
await deleteCommentMutation.mutateAsync(comment.id);
|
||||
editor?.commands.unsetComment(comment.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete comment:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditToggle() {
|
||||
setIsEditing(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} pb="xs">
|
||||
<Group>
|
||||
<UserAvatar color="blue" size="sm" avatarUrl={comment.creator.avatarUrl}
|
||||
name={comment.creator.name}
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" fw={500} lineClamp={1}>{comment.creator.name}</Text>
|
||||
|
||||
<div style={{ visibility: hovered ? 'visible' : 'hidden' }}>
|
||||
{/*!comment.parentCommentId && (
|
||||
<ResolveComment commentId={comment.id} pageId={comment.pageId} resolvedAt={comment.resolvedAt} />
|
||||
)*/}
|
||||
|
||||
<CommentMenu onEditComment={handleEditToggle} onDeleteComment={handleDeleteComment} />
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Text size="xs" fw={500} c="dimmed">
|
||||
{timeAgo(comment.createdAt)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<div>
|
||||
{!comment.parentCommentId && comment?.selection &&
|
||||
<Box className={classes.textSelection}>
|
||||
<Text size="sm">{comment?.selection}</Text>
|
||||
</Box>
|
||||
}
|
||||
|
||||
{
|
||||
!isEditing ?
|
||||
(<CommentEditor defaultContent={content} editable={false} />)
|
||||
:
|
||||
(<>
|
||||
<CommentEditor defaultContent={content} editable={true} onUpdate={(newContent) => setContent(newContent)}
|
||||
autofocus={true} />
|
||||
|
||||
<CommentActions onSave={handleUpdateComment} isLoading={isLoading} />
|
||||
</>)
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentListItem;
|
||||
105
apps/client/src/features/comment/components/comment-list.tsx
Normal file
105
apps/client/src/features/comment/components/comment-list.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Divider, Paper } from '@mantine/core';
|
||||
import CommentListItem from '@/features/comment/components/comment-list-item';
|
||||
import { useCommentsQuery, useCreateCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
|
||||
import CommentEditor from '@/features/comment/components/comment-editor';
|
||||
import CommentActions from '@/features/comment/components/comment-actions';
|
||||
import { useFocusWithin } from '@mantine/hooks';
|
||||
|
||||
function CommentList() {
|
||||
const { pageId } = useParams();
|
||||
const { data: comments, isLoading: isCommentsLoading, isError } = useCommentsQuery(pageId);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const createCommentMutation = useCreateCommentMutation();
|
||||
|
||||
if (isCommentsLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Error loading comments.</div>;
|
||||
}
|
||||
|
||||
if (!comments || comments.length === 0) {
|
||||
return <>No comments yet.</>;
|
||||
}
|
||||
|
||||
const renderComments = (comment) => {
|
||||
const handleAddReply = async (commentId, content) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const commentData = {
|
||||
pageId: comment.pageId,
|
||||
parentCommentId: comment.id,
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
|
||||
await createCommentMutation.mutateAsync(commentData);
|
||||
} catch (error) {
|
||||
console.error('Failed to post comment:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper shadow="sm" radius="md" p="sm" mb="sm" withBorder key={comment.id} data-comment-id={comment.id}>
|
||||
<div>
|
||||
<CommentListItem comment={comment} />
|
||||
<ChildComments comments={comments} parentId={comment.id} />
|
||||
</div>
|
||||
|
||||
<Divider my={4} />
|
||||
|
||||
<CommentEditorWithActions commentId={comment.id} onSave={handleAddReply} isLoading={isLoading} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{comments.filter(comment => comment.parentCommentId === null).map(renderComments)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ChildComments = ({ comments, parentId }) => {
|
||||
const getChildComments = (parentId) => {
|
||||
return comments.filter(comment => comment.parentCommentId === parentId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getChildComments(parentId).map(childComment => (
|
||||
<div key={childComment.id}>
|
||||
<CommentListItem comment={childComment} />
|
||||
<ChildComments comments={comments} parentId={childComment.id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => {
|
||||
const [content, setContent] = useState('');
|
||||
const { ref, focused } = useFocusWithin();
|
||||
const commentEditorRef = useRef(null);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(commentId, content);
|
||||
setContent('');
|
||||
commentEditorRef.current?.clearContent();
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<CommentEditor ref={commentEditorRef} onUpdate={setContent} editable={true} />
|
||||
{focused && <CommentActions onSave={handleSave} isLoading={isLoading} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CommentList;
|
||||
45
apps/client/src/features/comment/components/comment-menu.tsx
Normal file
45
apps/client/src/features/comment/components/comment-menu.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { ActionIcon, Menu } from '@mantine/core';
|
||||
import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
|
||||
type CommentMenuProps = {
|
||||
onEditComment: () => void;
|
||||
onDeleteComment: () => void;
|
||||
};
|
||||
|
||||
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
|
||||
|
||||
//@ts-ignore
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to delete this comment?',
|
||||
centered: true,
|
||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
||||
confirmProps: { color: 'red' },
|
||||
onConfirm: onDeleteComment,
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" style={{ border: 'none' }}>
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={onEditComment}
|
||||
leftSection={<IconEdit size={14} />}>
|
||||
Edit comment
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
Delete comment
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentMenu;
|
||||
@ -0,0 +1,45 @@
|
||||
.wrapper {
|
||||
padding: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.focused-thread {
|
||||
border: 2px solid #8d7249;
|
||||
}
|
||||
|
||||
.textSelection {
|
||||
margin-top: 4px;
|
||||
border-left: 2px solid var(--mantine-color-gray-6);
|
||||
padding: 8px;
|
||||
background: var(--mantine-color-gray-light);
|
||||
}
|
||||
|
||||
.commentEditor {
|
||||
|
||||
.focused {
|
||||
box-shadow: 0 0 0 2px var(--mantine-color-blue-3);
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 20vh;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 14px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { IconCircleCheck } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { useResolveCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
|
||||
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||
const resolveCommentMutation = useResolveCommentMutation();
|
||||
const isResolved = resolvedAt != null;
|
||||
const iconColor = isResolved ? 'green' : 'gray';
|
||||
|
||||
//@ts-ignore
|
||||
const openConfirmModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to resolve this comment thread?',
|
||||
centered: true,
|
||||
labels: { confirm: 'Confirm', cancel: 'Cancel' },
|
||||
onConfirm: handleResolveToggle,
|
||||
});
|
||||
|
||||
const handleResolveToggle = async () => {
|
||||
try {
|
||||
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
||||
//TODO: remove comment mark
|
||||
// Remove comment thread from state on resolve
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle resolved state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionIcon onClick={openConfirmModal} variant="default" style={{ border: 'none' }}>
|
||||
<IconCircleCheck size={20} stroke={2} color={iconColor} />
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResolveComment;
|
||||
96
apps/client/src/features/comment/queries/comment-query.ts
Normal file
96
apps/client/src/features/comment/queries/comment-query.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query';
|
||||
import {
|
||||
createComment,
|
||||
deleteComment, getPageComments,
|
||||
resolveComment,
|
||||
updateComment,
|
||||
} from '@/features/comment/services/comment-service';
|
||||
import { IComment, IResolveComment } from '@/features/comment/types/comment.types';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
export const RQ_KEY = (pageId: string) => ['comments', pageId];
|
||||
|
||||
export function useCommentsQuery(pageId: string): UseQueryResult<IComment[], Error> {
|
||||
return useQuery({
|
||||
queryKey: RQ_KEY(pageId),
|
||||
queryFn: () => getPageComments(pageId),
|
||||
enabled: !!pageId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => createComment(data),
|
||||
onSuccess: (data) => {
|
||||
const newComment = data;
|
||||
let comments = queryClient.getQueryData(RQ_KEY(data.pageId));
|
||||
if (comments) {
|
||||
comments = prevComments => [...prevComments, newComment];
|
||||
queryClient.setQueryData(RQ_KEY(data.pageId), comments);
|
||||
}
|
||||
|
||||
notifications.show({ message: 'Comment created successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: 'Error creating comment', color: 'red' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCommentMutation() {
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => updateComment(data),
|
||||
onSuccess: (data) => {
|
||||
notifications.show({ message: 'Comment updated successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: 'Failed to update comment', color: 'red' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCommentMutation(pageId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (commentId: string) => deleteComment(commentId),
|
||||
onSuccess: (data, variables) => {
|
||||
let comments = queryClient.getQueryData(RQ_KEY(pageId)) as IComment[];
|
||||
if (comments) {
|
||||
comments = comments.filter(comment => comment.id !== variables);
|
||||
queryClient.setQueryData(RQ_KEY(pageId), comments);
|
||||
}
|
||||
notifications.show({ message: 'Comment deleted successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: 'Failed to delete comment', color: 'red' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolveCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: IResolveComment) => resolveComment(data),
|
||||
onSuccess: (data: IComment, variables) => {
|
||||
|
||||
const currentComments = queryClient.getQueryData(RQ_KEY(data.pageId)) as IComment[];
|
||||
|
||||
if (currentComments) {
|
||||
const updatedComments = currentComments.map((comment) =>
|
||||
comment.id === variables.commentId ? { ...comment, ...data } : comment,
|
||||
);
|
||||
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
|
||||
}
|
||||
|
||||
notifications.show({ message: 'Comment resolved successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: 'Failed to resolve comment', color: 'red' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
31
apps/client/src/features/comment/services/comment-service.ts
Normal file
31
apps/client/src/features/comment/services/comment-service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import api from '@/lib/api-client';
|
||||
import { IComment, IResolveComment } from '@/features/comment/types/comment.types';
|
||||
|
||||
export async function createComment(data: Partial<IComment>): Promise<IComment> {
|
||||
const req = await api.post<IComment>('/comments/create', data);
|
||||
return req.data as IComment;
|
||||
}
|
||||
|
||||
export async function resolveComment(data: IResolveComment): Promise<IComment> {
|
||||
const req = await api.post<IComment>(`/comments/resolve`, data);
|
||||
return req.data as IComment;
|
||||
}
|
||||
|
||||
export async function updateComment(data: Partial<IComment>): Promise<IComment> {
|
||||
const req = await api.post<IComment>(`/comments/update`, data);
|
||||
return req.data as IComment;
|
||||
}
|
||||
|
||||
export async function getCommentById(id: string): Promise<IComment> {
|
||||
const req = await api.post<IComment>('/comments/view', { id });
|
||||
return req.data as IComment;
|
||||
}
|
||||
|
||||
export async function getPageComments(pageId: string): Promise<IComment[]> {
|
||||
const req = await api.post<IComment[]>('/comments', { pageId });
|
||||
return req.data as IComment[];
|
||||
}
|
||||
|
||||
export async function deleteComment(id: string): Promise<void> {
|
||||
await api.post('/comments/delete', { id });
|
||||
}
|
||||
31
apps/client/src/features/comment/types/comment.types.ts
Normal file
31
apps/client/src/features/comment/types/comment.types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { IUser } from '@/features/user/types/user.types';
|
||||
|
||||
export interface IComment {
|
||||
id: string;
|
||||
content: string;
|
||||
selection?: string;
|
||||
type?: string;
|
||||
creatorId: string;
|
||||
pageId: string;
|
||||
parentCommentId?: string;
|
||||
resolvedById?: string;
|
||||
resolvedAt?: Date;
|
||||
workspaceId: string;
|
||||
createdAt: Date;
|
||||
editedAt?: Date;
|
||||
deletedAt?: Date;
|
||||
creator: IUser
|
||||
}
|
||||
|
||||
export interface ICommentData {
|
||||
id: string;
|
||||
pageId: string;
|
||||
parentCommentId?: string;
|
||||
content: any;
|
||||
selection?: string;
|
||||
}
|
||||
|
||||
export interface IResolveComment {
|
||||
commentId: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user