chore: remove deprecated components related to issue activity/ comments. (#3465)

This commit is contained in:
Prateek Shourya 2024-01-25 14:04:38 +05:30 committed by GitHub
parent 03cbad5110
commit a104cc4814
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 213 additions and 1622 deletions

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -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>
);
});

View File

@ -1,3 +0,0 @@
export * from "./add-comment";
export * from "./comment-card";
export * from "./comment-reaction";

View File

@ -1,8 +1,6 @@
export * from "./attachment"; export * from "./attachment";
export * from "./comment";
export * from "./issue-modal"; export * from "./issue-modal";
export * from "./view-select"; export * from "./view-select";
export * from "./activity";
export * from "./delete-issue-modal"; export * from "./delete-issue-modal";
export * from "./description-form"; export * from "./description-form";
export * from "./issue-layouts"; export * from "./issue-layouts";

View File

@ -50,38 +50,10 @@ type Props = {
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
is_archived: boolean; is_archived: boolean;
is_editable: 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) => { export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
workspaceSlug,
projectId,
issueId,
issueOperations,
is_archived,
is_editable,
fieldsToShow = ["all"],
} = props;
// router // router
const router = useRouter(); const router = useRouter();
const { inboxIssueId } = router.query; const { inboxIssueId } = router.query;
@ -153,21 +125,19 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <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} /> <IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( <button
<button type="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"
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" onClick={handleCopyText}
onClick={handleCopyText} >
> <LinkIcon className="h-3.5 w-3.5" />
<LinkIcon className="h-3.5 w-3.5" /> </button>
</button>
)}
{is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( {is_editable && (
<button <button
type="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" 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,150 +153,137 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<h5 className="text-sm font-medium mt-6">Properties</h5> <h5 className="text-sm font-medium mt-6">Properties</h5>
{/* TODO: render properties using a common component */} {/* TODO: render properties using a common component */}
<div className={`mt-3 space-y-2 ${!is_editable ? "opacity-60" : ""}`}> <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" />
<span>State</span>
</div>
<StateDropdown
value={issue?.state_id ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
projectId={projectId?.toString() ?? ""}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName="text-sm"
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
<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" />
<span>Assignees</span>
</div>
<ProjectMemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable}
projectId={projectId?.toString() ?? ""}
placeholder="Add assignees"
multiple
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
<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" />
<span>Priority</span>
</div>
<PriorityDropdown
value={issue?.priority || undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={!is_editable}
buttonVariant="border-with-text"
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
buttonContainerClassName="w-full text-left"
buttonClassName="w-min h-auto whitespace-nowrap"
/>
</div>
<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" />
<span>Start date</span>
</div>
<DateDropdown
placeholder="Add start date"
value={issue.start_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
start_date: val ? renderFormattedPayloadDate(val) : null,
})
}
maxDate={maxDate ?? undefined}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
/>
</div>
<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" />
<span>Due date</span>
</div>
<DateDropdown
placeholder="Add due date"
value={issue.target_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
target_date: val ? renderFormattedPayloadDate(val) : null,
})
}
minDate={minDate ?? undefined}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
/>
</div>
{areEstimatesEnabledForCurrentProject && (
<div className="flex items-center gap-2 h-8"> <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"> <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" /> <Triangle className="h-4 w-4 flex-shrink-0" />
<span>State</span> <span>Estimate</span>
</div> </div>
<StateDropdown <EstimateDropdown
value={issue?.state_id ?? undefined} value={issue?.estimate_point !== null ? issue.estimate_point : null}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
projectId={projectId?.toString() ?? ""} projectId={projectId}
disabled={!is_editable} disabled={!is_editable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group" className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
buttonClassName="text-sm" buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
placeholder="None"
hideIcon
dropdownArrow dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/> />
</div> </div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( {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">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<span>Assignees</span>
</div>
<ProjectMemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable}
projectId={projectId?.toString() ?? ""}
placeholder="Add assignees"
multiple
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow
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" />
<span>Priority</span>
</div>
<PriorityDropdown
value={issue?.priority || undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={!is_editable}
buttonVariant="border-with-text"
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
buttonContainerClassName="w-full text-left"
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" />
<span>Start date</span>
</div>
<DateDropdown
placeholder="Add start date"
value={issue.start_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
start_date: val ? renderFormattedPayloadDate(val) : null,
})
}
maxDate={maxDate ?? undefined}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
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" />
<span>Due date</span>
</div>
<DateDropdown
placeholder="Add due date"
value={issue.target_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
target_date: val ? renderFormattedPayloadDate(val) : null,
})
}
minDate={minDate ?? undefined}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
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" />
<span>Estimate</span>
</div>
<EstimateDropdown
value={issue?.estimate_point !== null ? issue.estimate_point : null}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
}
projectId={projectId}
disabled={!is_editable}
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
placeholder="None"
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
<div className="flex items-center gap-2 h-8"> <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"> <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" /> <DiceIcon className="h-4 w-4 flex-shrink-0" />
@ -343,7 +300,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </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-2 h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300"> <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" /> <ContrastIcon className="h-4 w-4 flex-shrink-0" />
@ -360,117 +317,103 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( <div className="flex items-center gap-2 h-8">
<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">
<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" />
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" /> <span>Parent</span>
<span>Parent</span>
</div>
<IssueParentSelect
className="w-3/5 flex-grow h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={!is_editable}
/>
</div> </div>
)} <IssueParentSelect
className="w-3/5 flex-grow h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
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-2 min-h-8"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<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" />
<RelatedIcon className="h-4 w-4 flex-shrink-0" /> <span>Relates to</span>
<span>Relates to</span>
</div>
<IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="relates_to"
disabled={!is_editable}
/>
</div> </div>
)} <IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="relates_to"
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-2 min-h-8"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<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" />
<XCircle className="h-4 w-4 flex-shrink-0" /> <span>Blocking</span>
<span>Blocking</span>
</div>
<IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="blocking"
disabled={!is_editable}
/>
</div> </div>
)} <IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="blocking"
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-2 min-h-8"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<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" />
<CircleDot className="h-4 w-4 flex-shrink-0" /> <span>Blocked by</span>
<span>Blocked by</span>
</div>
<IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="blocked_by"
disabled={!is_editable}
/>
</div> </div>
)} <IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="blocked_by"
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-2 min-h-8"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<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" />
<CopyPlus className="h-4 w-4 flex-shrink-0" /> <span>Duplicate of</span>
<span>Duplicate of</span>
</div>
<IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="duplicate"
disabled={!is_editable}
/>
</div> </div>
)} <IssueRelationSelect
className="w-3/5 flex-grow min-h-8 h-full"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
relationKey="duplicate"
disabled={!is_editable}
/>
</div>
</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-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">
<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" />
<Tag className="h-4 w-4 flex-shrink-0" /> <span>Labels</span>
<span>Labels</span>
</div>
<div className="w-3/5 flex-grow min-h-8 h-full">
<IssueLabel
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={!is_editable}
/>
</div>
</div> </div>
)} <div className="w-3/5 flex-grow min-h-8 h-full">
<IssueLabel
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={!is_editable}
/>
</div>
</div>
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( <IssueLinkRoot
<IssueLinkRoot workspaceSlug={workspaceSlug}
workspaceSlug={workspaceSlug} projectId={projectId}
projectId={projectId} issueId={issueId}
issueId={issueId} disabled={!is_editable}
disabled={!is_editable} />
/>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -1,5 +0,0 @@
export * from "./card";
export * from "./comment-card";
export * from "./comment-editor";
export * from "./comment-reaction";
export * from "./view";

View File

@ -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>
);
};

View File

@ -1,5 +1,3 @@
export * from "./activity";
export * from "./reactions";
export * from "./issue-detail"; export * from "./issue-detail";
export * from "./properties"; export * from "./properties";
export * from "./root"; export * from "./root";

View File

@ -1,3 +0,0 @@
export * from "./preview";
export * from "./root";
export * from "./selector";

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};