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