mirror of
https://github.com/docmost/docmost.git
synced 2025-11-15 04:31:14 +10:00
sticky comment state tabs (EE)
This commit is contained in:
@ -38,6 +38,9 @@ export default function Aside() {
|
||||
{t(title)}
|
||||
</Text>
|
||||
|
||||
{tab === "comments" ? (
|
||||
<CommentListWithTabs />
|
||||
) : (
|
||||
<ScrollArea
|
||||
style={{ height: "85vh" }}
|
||||
scrollbarSize={5}
|
||||
@ -45,6 +48,7 @@ export default function Aside() {
|
||||
>
|
||||
<div style={{ paddingBottom: "200px" }}>{component}</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef, useCallback, memo, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Divider, Paper, Tabs, Badge, Text } from "@mantine/core";
|
||||
import { Divider, Paper, Tabs, Badge, Text, ScrollArea } from "@mantine/core";
|
||||
import CommentListItem from "@/features/comment/components/comment-list-item";
|
||||
import {
|
||||
useCommentsQuery,
|
||||
@ -43,7 +43,7 @@ function CommentListWithTabs() {
|
||||
|
||||
const canComment: boolean = spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Page,
|
||||
SpaceCaslSubject.Page
|
||||
);
|
||||
|
||||
// Separate active and resolved comments
|
||||
@ -53,14 +53,14 @@ function CommentListWithTabs() {
|
||||
}
|
||||
|
||||
const parentComments = comments.items.filter(
|
||||
(comment: IComment) => comment.parentCommentId === null,
|
||||
(comment: IComment) => comment.parentCommentId === null
|
||||
);
|
||||
|
||||
const active = parentComments.filter(
|
||||
(comment: IComment) => !comment.resolvedAt,
|
||||
(comment: IComment) => !comment.resolvedAt
|
||||
);
|
||||
const resolved = parentComments.filter(
|
||||
(comment: IComment) => comment.resolvedAt,
|
||||
(comment: IComment) => comment.resolvedAt
|
||||
);
|
||||
|
||||
return { activeComments: active, resolvedComments: resolved };
|
||||
@ -88,7 +88,7 @@ function CommentListWithTabs() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[createCommentMutation, page?.id],
|
||||
[createCommentMutation, page?.id]
|
||||
);
|
||||
|
||||
const renderComments = useCallback(
|
||||
@ -128,7 +128,7 @@ function CommentListWithTabs() {
|
||||
)}
|
||||
</Paper>
|
||||
),
|
||||
[comments, handleAddReply, isLoading],
|
||||
[comments, handleAddReply, isLoading]
|
||||
);
|
||||
|
||||
if (isCommentsLoading) {
|
||||
@ -141,28 +141,31 @@ function CommentListWithTabs() {
|
||||
|
||||
const totalComments = activeComments.length + resolvedComments.length;
|
||||
|
||||
// If not cloud/enterprise, show simple list without tabs
|
||||
if (!isCloudEE) {
|
||||
if (totalComments === 0) {
|
||||
return <>{t("No comments yet.")}</>;
|
||||
}
|
||||
|
||||
// If not cloud/enterprise, show simple list without tabs
|
||||
if (!isCloudEE) {
|
||||
return (
|
||||
<div>
|
||||
<ScrollArea style={{ height: "85vh" }} scrollbarSize={5} type="scroll">
|
||||
<div style={{ paddingBottom: "200px" }}>
|
||||
{comments?.items
|
||||
.filter((comment: IComment) => comment.parentCommentId === null)
|
||||
.map(renderComments)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="open" variant="default">
|
||||
<div style={{ height: "85vh", display: "flex", flexDirection: "column", marginTop: '-15px' }}>
|
||||
<Tabs defaultValue="open" variant="default" style={{ flex: "0 0 auto" }}>
|
||||
<Tabs.List justify="center">
|
||||
<Tabs.Tab
|
||||
value="open"
|
||||
leftSection={
|
||||
<Badge size="xs" variant="light" color="blue">
|
||||
<Badge size="sm" variant="light" color="blue">
|
||||
{activeComments.length}
|
||||
</Badge>
|
||||
}
|
||||
@ -172,7 +175,7 @@ function CommentListWithTabs() {
|
||||
<Tabs.Tab
|
||||
value="resolved"
|
||||
leftSection={
|
||||
<Badge size="xs" variant="light" color="green">
|
||||
<Badge size="sm" variant="light" color="green">
|
||||
{resolvedComments.length}
|
||||
</Badge>
|
||||
}
|
||||
@ -181,6 +184,12 @@ function CommentListWithTabs() {
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<ScrollArea
|
||||
style={{ flex: "1 1 auto", height: "calc(85vh - 60px)" }}
|
||||
scrollbarSize={5}
|
||||
type="scroll"
|
||||
>
|
||||
<div style={{ paddingBottom: "200px" }}>
|
||||
<Tabs.Panel value="open" pt="xs">
|
||||
{activeComments.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="md">
|
||||
@ -200,7 +209,10 @@ function CommentListWithTabs() {
|
||||
resolvedComments.map(renderComments)
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -219,9 +231,9 @@ const ChildComments = ({
|
||||
const getChildComments = useCallback(
|
||||
(parentId: string) =>
|
||||
comments.items.filter(
|
||||
(comment: IComment) => comment.parentCommentId === parentId,
|
||||
(comment: IComment) => comment.parentCommentId === parentId
|
||||
),
|
||||
[comments.items],
|
||||
[comments.items]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Mark, mergeAttributes } from "@tiptap/core";
|
||||
import { commentDecoration } from "./comment-decoration";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
|
||||
export interface ICommentOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
@ -123,7 +124,7 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
||||
const commentMark = node.marks.find(
|
||||
(mark) =>
|
||||
mark.type.name === this.name &&
|
||||
mark.attrs.commentId === commentId,
|
||||
mark.attrs.commentId === commentId
|
||||
);
|
||||
|
||||
if (commentMark) {
|
||||
@ -145,16 +146,20 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
||||
const commentMark = node.marks.find(
|
||||
(mark) =>
|
||||
mark.type.name === this.name &&
|
||||
mark.attrs.commentId === commentId,
|
||||
mark.attrs.commentId === commentId
|
||||
);
|
||||
|
||||
if (commentMark) {
|
||||
// Remove the existing mark and add a new one with updated resolved state
|
||||
tr = tr.removeMark(from, to, commentMark);
|
||||
tr = tr.addMark(from, to, this.type.create({
|
||||
tr = tr.addMark(
|
||||
from,
|
||||
to,
|
||||
this.type.create({
|
||||
commentId: commentMark.attrs.commentId,
|
||||
resolved: resolved,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -173,7 +178,7 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
||||
return [
|
||||
"span",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
class: resolved ? 'comment-mark resolved' : 'comment-mark',
|
||||
class: resolved ? "comment-mark resolved" : "comment-mark",
|
||||
"data-comment-id": commentId,
|
||||
...(resolved && { "data-resolved": "true" }),
|
||||
}),
|
||||
@ -184,12 +189,12 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
||||
const elem = document.createElement("span");
|
||||
|
||||
Object.entries(
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
|
||||
).forEach(([attr, val]) => elem.setAttribute(attr, val));
|
||||
|
||||
// Add resolved class if the comment is resolved
|
||||
if (resolved) {
|
||||
elem.classList.add('resolved');
|
||||
elem.classList.add("resolved");
|
||||
}
|
||||
|
||||
elem.addEventListener("click", (e) => {
|
||||
@ -208,9 +213,7 @@ export const Comment = Mark.create<ICommentOptions, ICommentStorage>({
|
||||
return elem;
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
addProseMirrorPlugins(): Plugin[] {
|
||||
// @ts-ignore
|
||||
return [commentDecoration()];
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user