forked from github/plane
chore: remove deprecated components related to issue activity/ comments. (#3465)
This commit is contained in:
parent
03cbad5110
commit
a104cc4814
@ -1,147 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
// ui
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "@plane/types";
|
||||
import { History } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
activity: IIssueActivity[] | undefined;
|
||||
handleCommentUpdate: (commentId: string, data: Partial<IIssueActivity>) => Promise<void>;
|
||||
handleCommentDelete: (commentId: string) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivitySection: React.FC<Props> = ({
|
||||
activity,
|
||||
handleCommentUpdate,
|
||||
handleCommentDelete,
|
||||
showAccessSpecifier = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
if (!activity)
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-4">
|
||||
{activity.map((activityItem, index) => {
|
||||
// determines what type of action is performed
|
||||
const message = activityItem.field ? <ActivityMessage activity={activityItem} /> : "created the issue.";
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{activity.length > 1 && index !== activity.length - 1 ? (
|
||||
<span
|
||||
className="absolute left-5 top-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name.charAt(0)
|
||||
: activityItem.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="break-words text-xs text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
{message}{" "}
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(activityItem.created_at)}, ${renderFormattedTime(
|
||||
activityItem.created_at
|
||||
)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">{calculateTimeAgo(activityItem.created_at)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
} else if ("comment_json" in activityItem)
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<CommentCard
|
||||
comment={activityItem as IIssueActivity}
|
||||
handleCommentDeletion={handleCommentDelete}
|
||||
onSubmit={handleCommentUpdate}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,126 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
const defaultValues: Partial<IIssueActivity> = {
|
||||
access: "INTERNAL",
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueActivity) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
type commentAccessType = {
|
||||
icon: any;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
};
|
||||
const commentAccess: commentAccessType[] = [
|
||||
{
|
||||
icon: Lock,
|
||||
key: "INTERNAL",
|
||||
label: "Private",
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
key: "EXTERNAL",
|
||||
label: "Public",
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAccessSpecifier = false }) => {
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IIssueActivity>({ defaultValues });
|
||||
|
||||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!formData.comment_html || isSubmitting) return;
|
||||
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div>
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
|
||||
commentAccessSpecifier={
|
||||
showAccessSpecifier
|
||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||
: undefined
|
||||
}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
submitButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
disabled={isSubmitting || disabled}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,194 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMention, useUser, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// icons
|
||||
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
type Props = {
|
||||
comment: IIssueActivity;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (commentId: string, data: Partial<IIssueActivity>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { comment, handleCommentDeletion, onSubmit, showAccessSpecifier = false, workspaceSlug } = props;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueActivity>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<IIssueActivity>) => {
|
||||
if (isSubmitting) return;
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(onEnter)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html") ?? ""}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit(onEnter)}
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute right-2.5 top-2.5 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html ?? ""}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,100 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useCommentReaction from "hooks/use-comment-reaction";
|
||||
// ui
|
||||
// import { ReactionSelector } from "components/core";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IssueCommentReaction } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
projectId?: string | string[];
|
||||
commentId: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const CommentReaction: FC<Props> = observer((props) => {
|
||||
const { projectId, commentId, readonly = false } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const { commentReactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useCommentReaction(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
commentId
|
||||
);
|
||||
|
||||
const handleReactionClick = (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !commentId) return;
|
||||
|
||||
const isSelected = commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
handleReactionDelete(reaction);
|
||||
} else {
|
||||
handleReactionCreate(reaction);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||
{/* {!readonly && (
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
commentReactions
|
||||
?.filter((reaction: IssueCommentReaction) => reaction.actor === currentUser?.id)
|
||||
.map((r: IssueCommentReaction) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
{Object.keys(groupedReactions || {}).map(
|
||||
(reaction) =>
|
||||
groupedReactions?.[reaction]?.length &&
|
||||
groupedReactions[reaction].length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
handleReactionClick(reaction);
|
||||
}}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
export * from "./add-comment";
|
||||
export * from "./comment-card";
|
||||
export * from "./comment-reaction";
|
@ -1,8 +1,6 @@
|
||||
export * from "./attachment";
|
||||
export * from "./comment";
|
||||
export * from "./issue-modal";
|
||||
export * from "./view-select";
|
||||
export * from "./activity";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./issue-layouts";
|
||||
|
@ -50,38 +50,10 @@ type Props = {
|
||||
issueOperations: TIssueOperations;
|
||||
is_archived: boolean;
|
||||
is_editable: boolean;
|
||||
fieldsToShow?: (
|
||||
| "state"
|
||||
| "assignee"
|
||||
| "priority"
|
||||
| "estimate"
|
||||
| "parent"
|
||||
| "blocker"
|
||||
| "blocked"
|
||||
| "startDate"
|
||||
| "dueDate"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "label"
|
||||
| "link"
|
||||
| "delete"
|
||||
| "all"
|
||||
| "subscribe"
|
||||
| "duplicate"
|
||||
| "relates_to"
|
||||
)[];
|
||||
};
|
||||
|
||||
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueOperations,
|
||||
is_archived,
|
||||
is_editable,
|
||||
fieldsToShow = ["all"],
|
||||
} = props;
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
@ -153,11 +125,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{currentUser && !is_archived && (fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
|
||||
{currentUser && !is_archived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
||||
@ -165,9 +136,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
||||
{is_editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
||||
@ -183,7 +153,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<h5 className="text-sm font-medium mt-6">Properties</h5>
|
||||
{/* TODO: render properties using a common component */}
|
||||
<div className={`mt-3 space-y-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@ -202,9 +171,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@ -228,9 +195,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
@ -246,9 +211,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
@ -272,9 +235,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
@ -298,10 +259,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||
areEstimatesEnabledForCurrentProject && (
|
||||
{areEstimatesEnabledForCurrentProject && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Triangle className="h-4 w-4 flex-shrink-0" />
|
||||
@ -309,9 +268,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<EstimateDropdown
|
||||
value={issue?.estimate_point !== null ? issue.estimate_point : null}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||
}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
|
||||
projectId={projectId}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
@ -326,7 +283,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
|
||||
{projectDetails?.module_view && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@ -343,7 +300,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && (
|
||||
{projectDetails?.cycle_view && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@ -360,7 +317,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
|
||||
@ -375,9 +331,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@ -392,9 +346,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||
@ -409,9 +361,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CircleDot className="h-4 w-4 flex-shrink-0" />
|
||||
@ -426,9 +376,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||
@ -443,10 +391,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<div className="flex items-center gap-2 min-h-8 py-2">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
@ -461,16 +407,13 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<IssueLinkRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,155 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { History } from "lucide-react";
|
||||
// packages
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { IssueCommentCard } from "./comment-card";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueActivity, IUser } from "@plane/types";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
|
||||
interface IIssueActivityCard {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueActivity: string[] | undefined;
|
||||
issueCommentUpdate: (comment: any) => void;
|
||||
issueCommentRemove: (commentId: string) => void;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueActivity,
|
||||
issueCommentUpdate,
|
||||
issueCommentRemove,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
} = props;
|
||||
|
||||
const { activity } = useIssueDetail();
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
{/* FIXME: --issue-detail-- */}
|
||||
{/* <ul role="list" className="-mb-4">
|
||||
{issueActivity ? (
|
||||
issueActivity.length > 0 &&
|
||||
issueActivity.map((activityId, index) => {
|
||||
// determines what type of action is performed
|
||||
const activityItem = activity.getActivityById(activityId) as IIssueActivity;
|
||||
const message = activityItem.field ? <ActivityMessage activity={activityItem} /> : <span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>;
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{issueActivity.length > 1 && index !== issueActivity.length - 1 ? (
|
||||
<span
|
||||
className="absolute left-5 top-5 -ml-[1.5px] h-full w-0.5 bg-custom-background-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-100 text-custom-text-200 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name.charAt(0)
|
||||
: activityItem.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="flex gap-1 break-words text-xs text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{message}
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(activityItem.created_at)}, ${renderFormattedTime(
|
||||
activityItem.created_at
|
||||
)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">{calculateTimeAgo(activityItem.created_at)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
} else if ("comment_html" in activityItem)
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<IssueCommentCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
comment={activityItem}
|
||||
onSubmit={issueCommentUpdate}
|
||||
handleCommentDeletion={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
// showAccessSpecifier={showAccessSpecifier}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="mb-3 space-y-3">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</ul> */}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,221 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// components
|
||||
import { IssueCommentReaction } from "./comment-reaction";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueActivity, IUser } from "@plane/types";
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
type IIssueCommentCard = {
|
||||
comment: IIssueActivity;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (data: Partial<IIssueActivity>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
};
|
||||
|
||||
export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
|
||||
const {
|
||||
comment,
|
||||
handleCommentDeletion,
|
||||
onSubmit,
|
||||
showAccessSpecifier = false,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
} = props;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueActivity>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const formSubmit = (formData: Partial<IIssueActivity>) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit({ id: comment.id, ...formData });
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</p>
|
||||
</div>
|
||||
|
||||
<div className="issue-comments-section p-0">
|
||||
<div className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(formSubmit)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html") ?? ""}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
onClick={handleSubmit(formSubmit)}
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute right-1.5 top-1 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html ?? ""}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
|
||||
<div className="mt-1">
|
||||
<IssueCommentReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
comment={comment}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit({ id: comment.id, access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit({ id: comment.id, access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,127 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
const defaultValues: Partial<IIssueActivity> = {
|
||||
access: "INTERNAL",
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type IIssueCommentEditor = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueActivity) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
type commentAccessType = {
|
||||
icon: any;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
};
|
||||
const commentAccess: commentAccessType[] = [
|
||||
{
|
||||
icon: Lock,
|
||||
key: "INTERNAL",
|
||||
label: "Private",
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
key: "EXTERNAL",
|
||||
label: "Public",
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
|
||||
const { disabled = false, onSubmit, showAccessSpecifier = false } = props;
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IIssueActivity>({ defaultValues });
|
||||
|
||||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!formData.comment_html || isSubmitting) return;
|
||||
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div className="space-y-2 py-2">
|
||||
<div className="h-full">
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
|
||||
commentAccessSpecifier={
|
||||
showAccessSpecifier
|
||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||
: undefined
|
||||
}
|
||||
submitButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
disabled={isSubmitting || disabled}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,57 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssuePeekOverviewReactions } from "components/issues";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
|
||||
interface IIssueCommentReaction {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: any;
|
||||
|
||||
comment: any;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueCommentReaction: FC<IIssueCommentReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, user, comment, issueCommentReactionCreate, issueCommentReactionRemove } =
|
||||
props;
|
||||
|
||||
const issueDetail = useIssueDetail();
|
||||
|
||||
const handleCommentReactionCreate = (reaction: string) => {
|
||||
if (issueCommentReactionCreate && comment?.id) issueCommentReactionCreate(comment?.id, reaction);
|
||||
};
|
||||
|
||||
const handleCommentReactionRemove = (reaction: string) => {
|
||||
if (issueCommentReactionRemove && comment?.id) issueCommentReactionRemove(comment?.id, reaction);
|
||||
};
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && issueId && comment && comment?.id
|
||||
? `ISSUE+PEEK_OVERVIEW_COMMENT_${comment?.id}`
|
||||
: null,
|
||||
() => {
|
||||
if (workspaceSlug && projectId && issueId && comment && comment.id) {
|
||||
issueDetail.fetchCommentReactions(workspaceSlug, projectId, comment?.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const issueReactions = issueDetail?.commentReaction.getCommentReactionsByCommentId(comment.id) || null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IssuePeekOverviewReactions
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={handleCommentReactionCreate}
|
||||
issueReactionRemove={handleCommentReactionRemove}
|
||||
position="top"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
export * from "./card";
|
||||
export * from "./comment-card";
|
||||
export * from "./comment-editor";
|
||||
export * from "./comment-reaction";
|
||||
export * from "./view";
|
@ -1,61 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { IssueActivityCard, IssueCommentEditor } from "components/issues";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueActivity: string[] | undefined;
|
||||
issueCommentCreate: (comment: any) => void;
|
||||
issueCommentUpdate: (comment: any) => void;
|
||||
issueCommentRemove: (commentId: string) => void;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
showCommentAccessSpecifier: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<Props> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueActivity,
|
||||
issueCommentCreate,
|
||||
issueCommentUpdate,
|
||||
issueCommentRemove,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
showCommentAccessSpecifier,
|
||||
} = props;
|
||||
|
||||
const handleAddComment = async (formData: any) => {
|
||||
if (!formData.comment_html) return;
|
||||
await issueCommentCreate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-custom-border-200 py-6">
|
||||
<div className="text-lg font-medium">Activity</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<IssueActivityCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
issueActivity={issueActivity}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
<IssueCommentEditor onSubmit={handleAddComment} showAccessSpecifier={showCommentAccessSpecifier} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,3 @@
|
||||
export * from "./activity";
|
||||
export * from "./reactions";
|
||||
export * from "./issue-detail";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
|
@ -1,3 +0,0 @@
|
||||
export * from "./preview";
|
||||
export * from "./root";
|
||||
export * from "./selector";
|
@ -1,48 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
interface IIssueReactionPreview {
|
||||
issueReactions: any;
|
||||
user: any;
|
||||
handleReaction: (reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
|
||||
const { issueReactions, user, handleReaction } = props;
|
||||
|
||||
const isUserReacted = (reactions: any) => {
|
||||
const userReaction = reactions?.find((reaction: any) => reaction.actor === user?.id);
|
||||
if (userReaction) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{Object.keys(issueReactions || {}).map(
|
||||
(reaction) =>
|
||||
issueReactions[reaction]?.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReaction(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1.5 rounded px-2 py-1 text-sm text-custom-text-100 ${
|
||||
isUserReacted(issueReactions[reaction])
|
||||
? `bg-custom-primary-100/10 hover:bg-custom-primary-100/30`
|
||||
: `bg-custom-background-90 hover:bg-custom-background-100/30`
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm">{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={`${
|
||||
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : ``
|
||||
}`}
|
||||
>
|
||||
{issueReactions[reaction].length}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { IssueReactionPreview, IssueReactionSelector } from "components/issues";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
|
||||
interface IIssueReaction {
|
||||
issueReactions: any;
|
||||
user: IUser | null;
|
||||
issueReactionCreate: (reaction: string) => void;
|
||||
issueReactionRemove: (reaction: string) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
|
||||
export const IssuePeekOverviewReactions: FC<IIssueReaction> = (props) => {
|
||||
const { issueReactions, user, issueReactionCreate, issueReactionRemove, position = "bottom" } = props;
|
||||
|
||||
const handleReaction = (reaction: string) => {
|
||||
const isReactionAvailable =
|
||||
issueReactions?.[reaction].find((_reaction: any) => _reaction.actor === user?.id) ?? false;
|
||||
|
||||
if (isReactionAvailable) issueReactionRemove(reaction);
|
||||
else issueReactionCreate(reaction);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap items-center gap-2">
|
||||
<IssueReactionSelector onSelect={handleReaction} position={position} />
|
||||
<IssueReactionPreview issueReactions={issueReactions} user={user} handleReaction={handleReaction} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,69 +0,0 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// icons
|
||||
import { SmilePlus } from "lucide-react";
|
||||
// constants
|
||||
import { issueReactionEmojis } from "constants/issue";
|
||||
|
||||
interface IIssueReactionSelector {
|
||||
size?: "sm" | "md" | "lg";
|
||||
position?: "top" | "bottom";
|
||||
onSelect: (reaction: any) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionSelector: FC<IIssueReactionSelector> = (props) => {
|
||||
const { size = "md", position = "top", onSelect } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover className="relative">
|
||||
{({ open, close: closePopover }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`${
|
||||
open ? "" : "bg-custom-background-80"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-80 transition-all hover:bg-custom-background-90 focus:outline-none`}
|
||||
>
|
||||
<span className={`flex items-center justify-center rounded px-2 py-1.5`}>
|
||||
<SmilePlus className={`${size === "sm" ? "h-3 w-3" : size === "md" ? "h-3.5 w-3.5" : "h-4 w-4"}`} />
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`absolute -left-2 z-10 overflow-hidden rounded border border-custom-border-200 bg-custom-sidebar-background-100 p-1 shadow-custom-shadow-sm ${
|
||||
position === "top" ? "-top-10" : "-bottom-10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-x-1">
|
||||
{issueReactionEmojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onSelect) onSelect(emoji);
|
||||
closePopover();
|
||||
}}
|
||||
className="flex h-6 w-6 select-none items-center justify-center rounded p-1 text-sm transition-all hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<div className="h-4 w-4">{renderEmoji(emoji)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user