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 "./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";
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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 "./issue-detail";
|
||||||
export * from "./properties";
|
export * from "./properties";
|
||||||
export * from "./root";
|
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