mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-10 04:22:00 +10:00
refactor react-query usage
This commit is contained in:
@ -5,6 +5,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
@ -10,13 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hocuspocus/provider": "^2.7.1",
|
||||
"@mantine/core": "^7.2.1",
|
||||
"@mantine/form": "^7.2.1",
|
||||
"@mantine/hooks": "^7.2.1",
|
||||
"@mantine/modals": "^7.2.1",
|
||||
"@mantine/spotlight": "^7.2.1",
|
||||
"@mantine/core": "^7.2.2",
|
||||
"@mantine/form": "^7.2.2",
|
||||
"@mantine/hooks": "^7.2.2",
|
||||
"@mantine/modals": "^7.2.2",
|
||||
"@mantine/notifications": "^7.2.2",
|
||||
"@mantine/spotlight": "^7.2.2",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-query": "^5.8.3",
|
||||
"@tiptap/extension-code-block": "^2.1.12",
|
||||
"@tiptap/extension-collaboration": "^2.1.12",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.1.12",
|
||||
@ -61,6 +62,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.8.3",
|
||||
"@types/js-cookie": "^3.0.4",
|
||||
"@types/node": "20.8.6",
|
||||
"@types/react": "^18.2.29",
|
||||
@ -74,7 +76,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"optics-ts": "^2.4.1",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-preset-mantine": "^1.9.0",
|
||||
"postcss-preset-mantine": "^1.11.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
|
||||
@ -1,23 +1,27 @@
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
import classes from './user-button.module.css';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
|
||||
export function UserButton() {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
|
||||
return (
|
||||
<UnstyledButton className={classes.user}>
|
||||
<Group>
|
||||
<Avatar
|
||||
src="https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=255&q=80"
|
||||
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-9.png"
|
||||
radius="xl"
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
Harriette Spoonlicker
|
||||
{currentUser.user.name}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
hspoonlicker@outlook.com
|
||||
{currentUser.user.email}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import t, { Toaster, useToasterStore } from "react-hot-toast";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function CustomToaster() {
|
||||
const { toasts } = useToasterStore();
|
||||
const TOAST_LIMIT = 3;
|
||||
const [toastLimit, setToastLimit] = useState<number>(TOAST_LIMIT);
|
||||
|
||||
useEffect(() => {
|
||||
toasts
|
||||
.filter((tt) => tt.visible)
|
||||
.filter((_, i) => i >= toastLimit)
|
||||
.forEach((tt) => {
|
||||
t.dismiss(tt.id);
|
||||
});
|
||||
}, [toastLimit, toasts]);
|
||||
|
||||
return <Toaster position={"top-right"}/>;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { login, register } from "@/features/auth/services/auth-service";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import { ILogin, IRegister } from "@/features/auth/types/auth.types";
|
||||
import toast from "react-hot-toast";
|
||||
import { useState } from 'react';
|
||||
import { login, register } from '@/features/auth/services/auth-service';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom';
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
import { ILogin, IRegister } from '@/features/auth/types/auth.types';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
export default function useAuth() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -22,10 +22,13 @@ export default function useAuth() {
|
||||
setIsLoading(false);
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate("/home");
|
||||
navigate('/home');
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
toast.error(err.response?.data.message)
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -38,10 +41,13 @@ export default function useAuth() {
|
||||
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate("/home");
|
||||
navigate('/home');
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
toast.error(err.response?.data.message)
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,7 +58,7 @@ export default function useAuth() {
|
||||
const handleLogout = async () => {
|
||||
setAuthToken(null);
|
||||
setCurrentUser(null);
|
||||
}
|
||||
};
|
||||
|
||||
return { signIn: handleSignIn, signUp: handleSignUp, isLoading, hasTokens };
|
||||
}
|
||||
|
||||
@ -1,17 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomFamily } from 'jotai/utils';
|
||||
import { IComment } from '@/features/comment/types/comment.types';
|
||||
|
||||
export const commentsAtom = atomFamily((pageId: string) => atom<IComment[]>([]));
|
||||
export const showCommentPopupAtom = atom(false);
|
||||
export const activeCommentIdAtom = atom<string | null>(null);
|
||||
export const draftCommentIdAtom = atom<string | null>(null);
|
||||
|
||||
export const deleteCommentAtom = atomFamily((pageId: string) => atom(
|
||||
null,
|
||||
(get, set, idToDelete: string) => {
|
||||
const currentPageComments = get(commentsAtom(pageId));
|
||||
const updatedComments = currentPageComments.filter(comment => comment.id !== idToDelete);
|
||||
set(commentsAtom(pageId), updatedComments);
|
||||
}
|
||||
));
|
||||
|
||||
@ -1,34 +1,22 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { commentsAtom } from '@/features/comment/atoms/comment-atom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getPageComments } from '@/features/comment/services/comment-service';
|
||||
import React from 'react';
|
||||
import classes from '@/features/comment/components/comment.module.css';
|
||||
import { Text } from '@mantine/core';
|
||||
import CommentList from '@/features/comment/components/comment-list';
|
||||
import { useCommentsQuery } from '@/features/comment/queries/comment';
|
||||
|
||||
export default function Comments() {
|
||||
const { pageId } = useParams();
|
||||
const [comments, setComments] = useAtom(commentsAtom(pageId));
|
||||
const { data, isLoading, isError } = useCommentsQuery(pageId);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchComments = async () => {
|
||||
try {
|
||||
const response = await getPageComments(pageId);
|
||||
setComments(response);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchComments();
|
||||
}, [pageId]);
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<Text mb="md" fw={500}>Comments</Text>
|
||||
|
||||
<CommentList comments={comments} />
|
||||
{data ? <CommentList comments={data} /> : 'No comments yet'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useClickOutside } from '@mantine/hooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import {
|
||||
activeCommentIdAtom,
|
||||
commentsAtom,
|
||||
draftCommentIdAtom,
|
||||
showCommentPopupAtom,
|
||||
} from '@/features/comment/atoms/comment-atom';
|
||||
@ -12,7 +11,7 @@ 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 useComment from '@/features/comment/hooks/use-comment';
|
||||
import { useCreateCommentMutation } from '@/features/comment/queries/comment';
|
||||
|
||||
interface CommentDialogProps {
|
||||
editor: Editor,
|
||||
@ -24,12 +23,11 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
const [, setShowCommentPopup] = useAtom<boolean>(showCommentPopupAtom);
|
||||
const [, setActiveCommentId] = useAtom<string | null>(activeCommentIdAtom);
|
||||
const [draftCommentId, setDraftCommentId] = useAtom<string | null>(draftCommentIdAtom);
|
||||
const [comments, setComments] = useAtom(commentsAtom(pageId));
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const useClickOutsideRef = useClickOutside(() => {
|
||||
handleDialogClose();
|
||||
});
|
||||
const { createCommentMutation } = useComment();
|
||||
const createCommentMutation = useCreateCommentMutation();
|
||||
const { isLoading } = createCommentMutation;
|
||||
|
||||
const handleDialogClose = () => {
|
||||
@ -43,17 +41,16 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
};
|
||||
|
||||
const handleAddComment = async () => {
|
||||
const selectedText = getSelectedText();
|
||||
const commentData = {
|
||||
id: draftCommentId,
|
||||
pageId: pageId,
|
||||
content: JSON.stringify(comment),
|
||||
selection: selectedText,
|
||||
};
|
||||
|
||||
try {
|
||||
const selectedText = getSelectedText();
|
||||
const commentData = {
|
||||
id: draftCommentId,
|
||||
pageId: pageId,
|
||||
content: JSON.stringify(comment),
|
||||
selection: selectedText,
|
||||
};
|
||||
|
||||
const createdComment = await createCommentMutation.mutateAsync(commentData);
|
||||
setComments(prevComments => [...prevComments, createdComment]);
|
||||
editor.chain().setComment(createdComment.id).unsetCommentDecoration().run();
|
||||
setActiveCommentId(createdComment.id);
|
||||
|
||||
|
||||
@ -1,45 +1,51 @@
|
||||
import { Group, Avatar, Text, Box } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
import classes from './comment.module.css';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { deleteCommentAtom } from '@/features/comment/atoms/comment-atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { timeAgo } from '@/lib/time-ago';
|
||||
import CommentEditor from '@/features/comment/components/comment-editor';
|
||||
import { editorAtom } from '@/features/editor/atoms/editorAtom';
|
||||
import CommentActions from '@/features/comment/components/comment-actions';
|
||||
import CommentMenu from '@/features/comment/components/comment-menu';
|
||||
import useComment from '@/features/comment/hooks/use-comment';
|
||||
import ResolveComment from '@/features/comment/components/resolve-comment';
|
||||
import { useHover } from '@mantine/hooks';
|
||||
import { useDeleteCommentMutation, useUpdateCommentMutation } from '@/features/comment/queries/comment';
|
||||
import { IComment } from '@/features/comment/types/comment.types';
|
||||
|
||||
function CommentListItem({ comment }) {
|
||||
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(editorAtom);
|
||||
const [content, setContent] = useState(comment.content);
|
||||
const { updateCommentMutation, deleteCommentMutation } = useComment();
|
||||
const { isLoading } = updateCommentMutation;
|
||||
const [, deleteComment] = useAtom(deleteCommentAtom(comment.pageId));
|
||||
const updateCommentMutation = useUpdateCommentMutation();
|
||||
const deleteCommentMutation = useDeleteCommentMutation(comment.pageId);
|
||||
|
||||
async function handleUpdateComment() {
|
||||
const commentToUpdate = {
|
||||
id: comment.id,
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
|
||||
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(comment.id);
|
||||
await deleteCommentMutation.mutateAsync(comment.id);
|
||||
editor?.commands.unsetComment(comment.id);
|
||||
deleteComment(comment.id); // Todo: unify code
|
||||
} catch (error) {
|
||||
console.error('Failed to delete comment:', error);
|
||||
}
|
||||
|
||||
@ -4,17 +4,16 @@ import CommentActions from '@/features/comment/components/comment-actions';
|
||||
import React, { useState } from 'react';
|
||||
import CommentListItem from '@/features/comment/components/comment-list-item';
|
||||
import { IComment } from '@/features/comment/types/comment.types';
|
||||
import useComment from '@/features/comment/hooks/use-comment';
|
||||
import { useAtom } from 'jotai';
|
||||
import { commentsAtom } from '@/features/comment/atoms/comment-atom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useFocusWithin } from '@mantine/hooks';
|
||||
import { useCreateCommentMutation } from '@/features/comment/queries/comment';
|
||||
|
||||
function CommentList({ comments }: IComment[]) {
|
||||
const { createCommentMutation } = useComment();
|
||||
const { isLoading } = createCommentMutation;
|
||||
const { pageId } = useParams();
|
||||
const [, setCommentsAtom] = useAtom(commentsAtom(pageId));
|
||||
interface CommentListProps {
|
||||
comments: IComment[];
|
||||
}
|
||||
|
||||
function CommentList({ comments }: CommentListProps) {
|
||||
const createCommentMutation = useCreateCommentMutation();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const getChildComments = (parentId) => {
|
||||
return comments.filter(comment => comment.parentCommentId === parentId);
|
||||
@ -54,14 +53,22 @@ function CommentList({ comments }: IComment[]) {
|
||||
|
||||
const renderComments = (comment) => {
|
||||
const handleAddReply = async (commentId, content) => {
|
||||
const commentData = {
|
||||
pageId: comment.pageId,
|
||||
parentCommentId: comment.id,
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const commentData = {
|
||||
pageId: comment.pageId,
|
||||
parentCommentId: comment.id,
|
||||
content: JSON.stringify(content),
|
||||
};
|
||||
|
||||
const createdComment = await createCommentMutation.mutateAsync(commentData);
|
||||
setCommentsAtom(prevComments => [...prevComments, createdComment]);
|
||||
await createCommentMutation.mutateAsync(commentData);
|
||||
} catch (error) {
|
||||
console.error('Failed to add reply:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
//setCommentsAtom(prevComments => [...prevComments, createdComment]);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { IconCircleCheck } from '@tabler/icons-react';
|
||||
|
||||
import useComment from '@/features/comment/hooks/use-comment';
|
||||
import { useAtom } from 'jotai';
|
||||
import { commentsAtom } from '@/features/comment/atoms/comment-atom';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { useResolveCommentMutation } from '@/features/comment/queries/comment';
|
||||
|
||||
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||
const [, setComments] = useAtom(commentsAtom(pageId));
|
||||
const { resolveCommentMutation } = useComment();
|
||||
const resolveCommentMutation = useResolveCommentMutation();
|
||||
const isResolved = resolvedAt != null;
|
||||
const iconColor = isResolved ? 'green' : 'gray';
|
||||
|
||||
@ -23,17 +19,9 @@ function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||
|
||||
const handleResolveToggle = async () => {
|
||||
try {
|
||||
const resolvedComment = await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
||||
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
||||
//TODO: remove comment mark
|
||||
// Remove comment thread from state on resolve
|
||||
|
||||
setComments((oldComments) =>
|
||||
oldComments.map((comment) =>
|
||||
comment.id === commentId
|
||||
? { ...comment, resolvedAt: resolvedComment.resolvedAt, resolvedById: resolvedComment.resolvedById }
|
||||
: comment,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle resolved state:', error);
|
||||
}
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
createComment,
|
||||
deleteComment,
|
||||
resolveComment,
|
||||
updateComment,
|
||||
} from '@/features/comment/services/comment-service';
|
||||
import { IComment, IResolveComment } from '@/features/comment/types/comment.types';
|
||||
|
||||
export default function useComment() {
|
||||
|
||||
const createMutation = useMutation(
|
||||
(data: Partial<IComment>) => createComment(data),
|
||||
{
|
||||
onSuccess: (data: IComment) => {
|
||||
toast.success('Comment created successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error creating comment: ${error.message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(data: Partial<IComment>) => updateComment(data),
|
||||
{
|
||||
onSuccess: (data: IComment) => {
|
||||
toast.success('Comment updated successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error updating comment: ${error.message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const resolveMutation = useMutation(
|
||||
(data: IResolveComment) => resolveComment(data),
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to perform resolve action: ${error.message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const deleteMutation = useMutation(
|
||||
(id: string) => deleteComment(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Comment deleted successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Error deleting comment: ${error.message}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
createCommentMutation: createMutation,
|
||||
updateCommentMutation: updateMutation,
|
||||
resolveCommentMutation: resolveMutation,
|
||||
deleteCommentMutation: deleteMutation.mutateAsync,
|
||||
};
|
||||
}
|
||||
96
client/src/features/comment/queries/comment.ts
Normal file
96
client/src/features/comment/queries/comment.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) => ['comment', 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));
|
||||
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));
|
||||
|
||||
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' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import { EditorBubbleMenu } from '@/features/editor/components/bubble-menu/bubbl
|
||||
import { Document } from '@tiptap/extension-document';
|
||||
import { Text } from '@tiptap/extension-text';
|
||||
import { Heading } from '@tiptap/extension-heading';
|
||||
import usePage from '@/features/page/hooks/use-page';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { pageAtom } from '@/features/page/atoms/page-atom';
|
||||
import { IPage } from '@/features/page/types/page.types';
|
||||
@ -24,6 +23,7 @@ import { activeCommentIdAtom, showCommentPopupAtom } from '@/features/comment/at
|
||||
import CommentDialog from '@/features/comment/components/comment-dialog';
|
||||
import { editorAtom } from '@/features/editor/atoms/editorAtom';
|
||||
import { collabExtensions, mainExtensions } from '@/features/editor/extensions';
|
||||
import { useUpdatePageMutation } from '@/features/page/queries/page';
|
||||
|
||||
interface EditorProps {
|
||||
pageId: string,
|
||||
@ -93,7 +93,7 @@ function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) {
|
||||
const [page, setPage] = useAtom(pageAtom<IPage>(pageId));
|
||||
const [debouncedTitleState, setDebouncedTitleState] = useState('');
|
||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000);
|
||||
const { updatePageMutation } = usePage();
|
||||
const updatePageMutation = useUpdatePageMutation();
|
||||
const [desktopAsideOpened, setDesktopAsideOpened] = useAtom<boolean>(desktopAsideAtom);
|
||||
const [activeCommentId, setActiveCommentId] = useAtom<string | null>(activeCommentIdAtom);
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom<boolean>(showCommentPopupAtom);
|
||||
@ -127,7 +127,7 @@ function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedTitle !== '') {
|
||||
updatePageMutation({ id: pageId, title: debouncedTitle });
|
||||
updatePageMutation.mutate({ id: pageId, title: debouncedTitle });
|
||||
}
|
||||
}, [debouncedTitle]);
|
||||
|
||||
|
||||
@ -3,11 +3,10 @@ import { format } from 'date-fns';
|
||||
import classes from './home.module.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PageListSkeleton from '@/features/home/components/page-list-skeleton';
|
||||
import usePage from '@/features/page/hooks/use-page';
|
||||
import { useRecentChangesQuery } from '@/features/page/queries/page';
|
||||
|
||||
function RecentChanges() {
|
||||
const { recentPagesQuery } = usePage();
|
||||
const { data, isLoading, isError } = recentPagesQuery;
|
||||
const { data, isLoading, isError } = useRecentChangesQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
@ -20,9 +19,9 @@ function RecentChanges() {
|
||||
return (
|
||||
<div>
|
||||
{data.map((page) => (
|
||||
<>
|
||||
<div key={page.id}>
|
||||
<UnstyledButton component={Link} to={`/p/${page.id}`}
|
||||
className={classes.page} p="xs" key={page.id}>
|
||||
className={classes.page} p="xs">
|
||||
<Group wrap="noWrap">
|
||||
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
@ -37,7 +36,7 @@ function RecentChanges() {
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<Divider />
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import { createPage, deletePage, getPageById, getRecentChanges, updatePage } from '@/features/page/services/page-service';
|
||||
import { IPage } from '@/features/page/types/page.types';
|
||||
|
||||
export default function usePage(pageId?: string) {
|
||||
const createMutation = useMutation(
|
||||
(data: Partial<IPage>) => createPage(data),
|
||||
);
|
||||
|
||||
const pageQueryResult: UseQueryResult<IPage, unknown> = useQuery(
|
||||
['page', pageId],
|
||||
() => getPageById(pageId as string),
|
||||
{
|
||||
enabled: !!pageId,
|
||||
},
|
||||
);
|
||||
|
||||
const recentPagesQuery: UseQueryResult<IPage[], unknown> = useQuery(
|
||||
['recentChanges'],
|
||||
() => getRecentChanges()
|
||||
);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(data: Partial<IPage>) => updatePage(data),
|
||||
);
|
||||
|
||||
const removeMutation = useMutation(
|
||||
(id: string) => deletePage(id),
|
||||
);
|
||||
|
||||
return {
|
||||
pageQuery: pageQueryResult,
|
||||
recentPagesQuery: recentPagesQuery,
|
||||
create: createMutation.mutate,
|
||||
updatePageMutation: updateMutation.mutate,
|
||||
remove: removeMutation.mutate,
|
||||
};
|
||||
}
|
||||
45
client/src/features/page/queries/page.ts
Normal file
45
client/src/features/page/queries/page.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createPage,
|
||||
deletePage,
|
||||
getPageById,
|
||||
getRecentChanges,
|
||||
updatePage,
|
||||
} from '@/features/page/services/page-service';
|
||||
import { IPage } from '@/features/page/types/page.types';
|
||||
|
||||
const RECENT_CHANGES_KEY = ['recentChanges'];
|
||||
|
||||
export function usePageQuery(pageId: string): UseQueryResult<IPage, Error> {
|
||||
return useQuery({
|
||||
queryKey: ['page', pageId],
|
||||
queryFn: () => getPageById(pageId),
|
||||
enabled: !!pageId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
|
||||
return useQuery({
|
||||
queryKey: RECENT_CHANGES_KEY,
|
||||
queryFn: () => getRecentChanges(),
|
||||
refetchOnMount: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePageMutation() {
|
||||
return useMutation<IPage, Error, Partial<IPage>>({
|
||||
mutationFn: (data) => createPage(data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdatePageMutation() {
|
||||
return useMutation<IPage, Error, Partial<IPage>>({
|
||||
mutationFn: (data) => updatePage(data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeletePageMutation() {
|
||||
return useMutation({
|
||||
mutationFn: (pageId: string) => deletePage(pageId),
|
||||
});
|
||||
}
|
||||
@ -11,13 +11,13 @@ import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom';
|
||||
import { createPage, deletePage, movePage } from '@/features/page/services/page-service';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IMovePage } from '@/features/page/types/page.types';
|
||||
import { useNavigate} from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TreeNode } from '@/features/page/tree/types';
|
||||
import usePage from '@/features/page/hooks/use-page';
|
||||
import { useUpdatePageMutation } from '@/features/page/queries/page';
|
||||
|
||||
export function usePersistence<T>() {
|
||||
const [data, setData] = useAtom(treeDataAtom);
|
||||
const { updatePageMutation } = usePage();
|
||||
const updatePageMutation = useUpdatePageMutation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tree = useMemo(() => new SimpleTree<TreeNode>(data), [data]);
|
||||
@ -34,7 +34,7 @@ export function usePersistence<T>() {
|
||||
const afterId = currentTreeData[newDragIndex - 1]?.id || null;
|
||||
const beforeId = !afterId && currentTreeData[newDragIndex + 1]?.id || null;
|
||||
|
||||
const params: IMovePage= {
|
||||
const params: IMovePage = {
|
||||
id: args.dragIds[0],
|
||||
after: afterId,
|
||||
before: beforeId,
|
||||
@ -42,7 +42,7 @@ export function usePersistence<T>() {
|
||||
};
|
||||
|
||||
const payload = Object.fromEntries(
|
||||
Object.entries(params).filter(([key, value]) => value !== null && value !== undefined)
|
||||
Object.entries(params).filter(([key, value]) => value !== null && value !== undefined),
|
||||
);
|
||||
|
||||
try {
|
||||
@ -57,7 +57,7 @@ export function usePersistence<T>() {
|
||||
setData(tree.data);
|
||||
|
||||
try {
|
||||
updatePageMutation({ id, title: name });
|
||||
updatePageMutation.mutateAsync({ id, title: name });
|
||||
} catch (error) {
|
||||
console.error('Error updating page title:', error);
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
import { updateUser } from '@/features/user/services/user-service';
|
||||
import { IUser } from '@/features/user/types/user.types';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { TextInput, Button } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(40).nonempty('Your name cannot be blank'),
|
||||
@ -35,10 +35,15 @@ export default function AccountNameForm() {
|
||||
try {
|
||||
const updatedUser = await updateUser(data);
|
||||
setUser(updatedUser);
|
||||
toast.success('Updated successfully');
|
||||
notifications.show({
|
||||
message: 'Updated successfully',
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
toast.error('Failed to update data.');
|
||||
notifications.show({
|
||||
message: 'Failed to update data',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
@ -53,10 +58,10 @@ export default function AccountNameForm() {
|
||||
variant="filled"
|
||||
{...form.getInputProps('name')}
|
||||
rightSection={
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
}
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
import { useAtom } from 'jotai';
|
||||
import * as z from 'zod';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
import { focusAtom } from 'jotai-optics';
|
||||
import { updateWorkspace } from '@/features/workspace/services/workspace-service';
|
||||
import { IWorkspace } from '@/features/workspace/types/workspace.types';
|
||||
import { TextInput, Button } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().nonempty('Workspace name cannot be blank'),
|
||||
@ -35,14 +35,15 @@ export default function WorkspaceNameForm() {
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace(data);
|
||||
setWorkspace(updatedWorkspace);
|
||||
toast.success('Updated successfully');
|
||||
notifications.show({ message: 'Updated successfully' });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
toast.error('Failed to update data.');
|
||||
notifications.show({
|
||||
message: 'Failed to update data',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/spotlight/styles.css';
|
||||
|
||||
import '@mantine/notifications/styles.css';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import { theme } from '@/theme';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { TanstackProvider } from '@/components/providers/tanstack-provider';
|
||||
import CustomToaster from '@/components/ui/custom-toaster';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
|
||||
@ -17,10 +17,10 @@ root.render(
|
||||
<BrowserRouter>
|
||||
<MantineProvider theme={theme}>
|
||||
<ModalsProvider>
|
||||
<TanstackProvider>
|
||||
<App />
|
||||
<CustomToaster />
|
||||
</TanstackProvider>
|
||||
<TanstackProvider>
|
||||
<Notifications />
|
||||
<App />
|
||||
</TanstackProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</BrowserRouter>,
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import usePage from '@/features/page/hooks/use-page';
|
||||
import { useAtom } from 'jotai';
|
||||
import Editor from '@/features/editor/editor';
|
||||
import { pageAtom } from '@/features/page/atoms/page-atom';
|
||||
import { usePageQuery } from '@/features/page/queries/page';
|
||||
|
||||
export default function Page() {
|
||||
const { pageId } = useParams();
|
||||
const [, setPage] = useAtom(pageAtom(pageId));
|
||||
const { pageQuery } = usePage(pageId);
|
||||
const { data, isLoading, isError } = usePageQuery(pageId);
|
||||
|
||||
useEffect(() => {
|
||||
if (pageQuery.data) {
|
||||
setPage(pageQuery.data);
|
||||
if (data) {
|
||||
setPage(data);
|
||||
}
|
||||
}, [pageQuery.data, pageQuery.isLoading, setPage, pageId]);
|
||||
}, [data, isLoading, setPage, pageId]);
|
||||
|
||||
if (pageQuery.isLoading) {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (pageQuery.isError) {
|
||||
if (isError) {
|
||||
return <div>Error fetching page data.</div>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user