chore: issue activity, comments, and comment reaction store and component restructure (#3428)
* fix: issue activity and comment change * chore: posthog enabled * chore: comment creation in activity * chore: comment crud in store mutation * fix: issue activity/ comments `disable` and `showAccessSpecifier` logic. * chore: comment reaction serializer change * conflicts: merge conflicts resolved * conflicts: merge conflicts resolved * chore: add issue activity/ comments to peek-overview. * imporve `showAccessIdentifier` logic. * chore: remove quotes from issue activity. * chore: use `projectLabels` instead of `workspaceLabels` in labels activity. * fix: project publish `is_deployed` not updating bug. * cleanup * fix: posthog enabled * fix: typos and the comment endpoint updates * fix: issue activity icons update --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
parent
bb50df0dff
commit
f88109ef04
66 changed files with 2555 additions and 395 deletions
|
|
@ -187,109 +187,6 @@ const activityDetails: {
|
|||
},
|
||||
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocked_by: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is being blocked by{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue being blocked by issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
<span className="flex-shrink-0">added this issue to the cycle </span>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
<span className="flex-shrink-0">set the cycle to </span>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the cycle{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.old_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
duplicate: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue as duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue as a duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
|
|
@ -448,6 +345,53 @@ const activityDetails: {
|
|||
},
|
||||
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
<span className="flex-shrink-0">added this issue to the cycle </span>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
<span className="flex-shrink-0">set the cycle to </span>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the cycle{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.old_value}</span>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.verb === "created")
|
||||
|
|
@ -577,6 +521,77 @@ const activityDetails: {
|
|||
},
|
||||
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocked_by: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue is being blocked by{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue being blocked by issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
duplicate: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked this issue as duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed this issue as a duplicate of{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
|
|
@ -596,9 +611,7 @@ const activityDetails: {
|
|||
return (
|
||||
<>
|
||||
set the start date to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderFormattedDate(activity.new_value)}
|
||||
</span>
|
||||
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
|
@ -611,21 +624,6 @@ const activityDetails: {
|
|||
},
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
|
|
@ -645,9 +643,7 @@ const activityDetails: {
|
|||
return (
|
||||
<>
|
||||
set the due date to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderFormattedDate(activity.new_value)}
|
||||
</span>
|
||||
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
|
|
|||
|
|
@ -88,41 +88,43 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
|
|||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!workspaceSlug || !issueDetails || !currentUser) return;
|
||||
|
||||
await issueCommentService
|
||||
.createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
||||
.then((res) => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
postHogEventTracker(
|
||||
"COMMENT_ADDED",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Comment could not be posted. Please try again.",
|
||||
})
|
||||
);
|
||||
/* FIXME: Replace this with the new issue activity component --issue-detail-- */
|
||||
// await issueCommentService
|
||||
// .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
||||
// .then((res) => {
|
||||
// mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
// postHogEventTracker(
|
||||
// "COMMENT_ADDED",
|
||||
// {
|
||||
// ...res,
|
||||
// state: "SUCCESS",
|
||||
// },
|
||||
// {
|
||||
// isGrouping: true,
|
||||
// groupType: "Workspace_metrics",
|
||||
// groupId: currentWorkspace?.id!,
|
||||
// }
|
||||
// );
|
||||
// })
|
||||
// .catch(() =>
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Comment could not be posted. Please try again.",
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
{/* FIXME: Replace this with the new issue activity component --issue-detail-- */}
|
||||
{/* <h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
/>
|
||||
<AddComment onSubmit={handleAddComment} />
|
||||
<AddComment onSubmit={handleAddComment} /> */}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export const IssueActivitySection: React.FC<Props> = ({
|
|||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
|
|
@ -97,6 +98,7 @@ export const IssueActivitySection: React.FC<Props> = ({
|
|||
</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" ? (
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||
<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">
|
||||
|
|
@ -146,6 +147,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
|||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<CustomSearchSelect
|
||||
value={issue?.cycle_id}
|
||||
value={issue?.cycle_id || undefined}
|
||||
onChange={(value: any) => handleIssueCycleChange(value)}
|
||||
options={options}
|
||||
customButton={
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityList } from "./activity/activity-list";
|
||||
import { IssueCommentCard } from "./comments/comment-card";
|
||||
// types
|
||||
import { TActivityOperations } from "./root";
|
||||
|
||||
type TIssueActivityCommentRoot = {
|
||||
workspaceSlug: string;
|
||||
issueId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer((props) => {
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityCommentByIssueId },
|
||||
comment: {},
|
||||
} = useIssueDetail();
|
||||
|
||||
const activityComments = getActivityCommentByIssueId(issueId);
|
||||
|
||||
if (!activityComments || (activityComments && activityComments.length <= 0)) return <></>;
|
||||
return (
|
||||
<div>
|
||||
{activityComments.map((activityComment, index) =>
|
||||
activityComment.activity_type === "COMMENT" ? (
|
||||
<IssueCommentCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
commentId={activityComment.id}
|
||||
activityOperations={activityOperations}
|
||||
ends={index === 0 ? "top" : index === activityComments.length - 1 ? "bottom" : undefined}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
/>
|
||||
) : activityComment.activity_type === "ACTIVITY" ? (
|
||||
<IssueActivityList
|
||||
activityId={activityComment.id}
|
||||
ends={index === 0 ? "top" : index === activityComments.length - 1 ? "bottom" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
|
||||
type TIssueArchivedAtActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueArchivedAtActivity: FC<TIssueArchivedAtActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
{activity.new_value === "restore" ? `restored the issue` : `archived the issue`}.
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
// icons
|
||||
import { UserGroupIcon } from "@plane/ui";
|
||||
|
||||
type TIssueAssigneeActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueAssigneeActivity: FC<TIssueAssigneeActivity> = observer((props) => {
|
||||
const { activityId, ends, showIssue = true } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<UserGroupIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.old_value === "" ? `added a new assignee ` : `removed the assignee `}
|
||||
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center font-medium text-custom-text-100 hover:underline capitalize"
|
||||
>
|
||||
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
|
||||
</a>
|
||||
|
||||
{showIssue && (activity.old_value === "" ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Paperclip } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueAttachmentActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueAttachmentActivity: FC<TIssueAttachmentActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Paperclip size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.verb === "created" ? `uploaded a new ` : `removed an attachment`}
|
||||
{activity.verb === "created" && (
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
attachment
|
||||
</a>
|
||||
)}
|
||||
{showIssue && (activity.verb === "created" ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// icons
|
||||
import { ContrastIcon } from "@plane/ui";
|
||||
|
||||
type TIssueCycleActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueCycleActivity: FC<TIssueCycleActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<ContrastIcon className="h-4 w-4 flex-shrink-0 text-[#6b7280]" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.verb === "created" ? (
|
||||
<>
|
||||
<span>added this issue to the cycle </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
) : activity.verb === "updated" ? (
|
||||
<>
|
||||
<span>set the cycle to </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate"> {activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>removed the issue from the cycle </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate"> {activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// icons
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
|
||||
type TIssueDefaultActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
activityId={activityId}
|
||||
icon={<LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />}
|
||||
ends={ends}
|
||||
>
|
||||
<>{activity.verb === "created" ? " created the issue." : " deleted an issue."}</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueDescriptionActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueDescriptionActivity: FC<TIssueDescriptionActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
updated the description
|
||||
{showIssue ? ` of ` : ``}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Triangle } from "lucide-react";
|
||||
// hooks
|
||||
import { useEstimate, useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueEstimateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
|
||||
const estimateValue = getEstimatePointValue(Number(activity.new_value));
|
||||
const currentPoint = Number(activity.new_value) + 1;
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Triangle size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.new_value ? `set the estimate point to ` : `removed the estimate point `}
|
||||
{activity.new_value && (
|
||||
<>
|
||||
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{areEstimatesEnabledForCurrentProject
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
|
||||
</>
|
||||
)}
|
||||
{showIssue && (activity.new_value ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { FC, ReactNode } from "react";
|
||||
import { Network } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { IssueUser } from "../";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||
|
||||
type TIssueActivityBlockComponent = {
|
||||
icon?: ReactNode;
|
||||
activityId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (props) => {
|
||||
const { icon, activityId, ends, children } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center gap-3 text-xs ${
|
||||
ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`
|
||||
}`}
|
||||
>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden={true} />
|
||||
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-10 bg-custom-background-80 text-custom-text-200">
|
||||
{icon ? icon : <Network className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
<div className="w-full text-custom-text-200">
|
||||
<IssueUser activityId={activityId} />
|
||||
<span> {children} </span>
|
||||
<span>
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap"> {calculateTimeAgo(activity.created_at)}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
type TIssueLink = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const IssueLink: FC<TIssueLink> = (props) => {
|
||||
const { activityId } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
|
||||
<a
|
||||
aria-disabled={activity.issue === null}
|
||||
href={`${
|
||||
activity.issue_detail
|
||||
? `/${activity.workspace_detail?.slug}/projects/${activity.project}/issues/${activity.issue}`
|
||||
: "#"
|
||||
}`}
|
||||
target={activity.issue === null ? "_self" : "_blank"}
|
||||
rel={activity.issue === null ? "" : "noopener noreferrer"}
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}{" "}
|
||||
<span className="font-normal">{activity.issue_detail?.name}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { FC } from "react";
|
||||
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// ui
|
||||
|
||||
type TIssueUser = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const IssueUser: FC<TIssueUser> = (props) => {
|
||||
const { activityId } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<a
|
||||
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
|
||||
className="hover:underline text-custom-text-100 font-medium capitalize"
|
||||
>
|
||||
{activity.actor_detail?.display_name}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export * from "./default";
|
||||
export * from "./name";
|
||||
export * from "./description";
|
||||
export * from "./state";
|
||||
export * from "./assignee";
|
||||
export * from "./priority";
|
||||
export * from "./estimate";
|
||||
export * from "./parent";
|
||||
export * from "./relation";
|
||||
export * from "./start_date";
|
||||
export * from "./target_date";
|
||||
export * from "./cycle";
|
||||
export * from "./module";
|
||||
export * from "./label";
|
||||
export * from "./link";
|
||||
export * from "./attachment";
|
||||
export * from "./archived-at";
|
||||
|
||||
// helpers
|
||||
export * from "./helpers/activity-block";
|
||||
export * from "./helpers/issue-user";
|
||||
export * from "./helpers/issue-link";
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Tag } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useLabel } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueLabelActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueLabelActivity: FC<TIssueLabelActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
const { projectLabels } = useLabel();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Tag size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.old_value === "" ? `added a new label ` : `removed the label `}
|
||||
{activity.old_value === "" ? (
|
||||
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: projectLabels?.find((l) => l.id === activity.new_identifier)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: projectLabels?.find((l) => l.id === activity.old_identifier)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
)}
|
||||
{showIssue && (activity.old_value === "" ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueLinkActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueLinkActivity: FC<TIssueLinkActivity> = observer((props) => {
|
||||
const { activityId, showIssue = false, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.verb === "created" ? (
|
||||
<>
|
||||
<span>added this </span>
|
||||
<a
|
||||
href={`${activity.new_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</>
|
||||
) : activity.verb === "updated" ? (
|
||||
<>
|
||||
<span>updated the </span>
|
||||
<a
|
||||
href={`${activity.old_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>removed this </span>
|
||||
<a
|
||||
href={`${activity.old_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{showIssue && (activity.verb === "created" ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// icons
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
|
||||
type TIssueModuleActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<DiceIcon className="h-4 w-4 flex-shrink-0 text-[#6b7280]" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.verb === "created" ? (
|
||||
<>
|
||||
<span>added this issue to the module </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate">{activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
) : activity.verb === "updated" ? (
|
||||
<>
|
||||
<span>set the module to </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate"> {activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>removed the issue from the module </span>
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/modules/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="truncate"> {activity.new_value}</span>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
|
||||
type TIssueNameActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueNameActivity: FC<TIssueNameActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>set the name to {activity.new_value}.</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { LayoutPanelTop } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssueParentActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueParentActivity: FC<TIssueParentActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<LayoutPanelTop size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.new_value ? `set the parent to ` : `removed the parent `}
|
||||
{activity.new_value ? (
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
) : (
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
)}
|
||||
{showIssue && (activity.new_value ? ` for ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Signal } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
type TIssuePriorityActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssuePriorityActivity: FC<TIssuePriorityActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Signal size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
set the priority to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue ? ` for ` : ``}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// component helpers
|
||||
import { issueRelationObject } from "components/issues/issue-detail/relation-select";
|
||||
// types
|
||||
import { TIssueRelationTypes } from "@plane/types";
|
||||
|
||||
type TIssueRelationActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueRelationActivity: FC<TIssueRelationActivity> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={activity.field ? issueRelationObject[activity.field as TIssueRelationTypes].icon(14) : <></>}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.field === "blocking" &&
|
||||
(activity.old_value === "" ? `marked this issue is blocking issue ` : `removed the blocking issue `)}
|
||||
{activity.field === "blocked_by" &&
|
||||
(activity.old_value === ""
|
||||
? `marked this issue is being blocked by `
|
||||
: `removed this issue being blocked by issue `)}
|
||||
{activity.field === "duplicate" &&
|
||||
(activity.old_value === "" ? `marked this issue as duplicate of ` : `removed this issue as a duplicate of `)}
|
||||
{activity.field === "relates_to" &&
|
||||
(activity.old_value === "" ? `marked that this issue relates to ` : `removed the relation from `)}
|
||||
|
||||
{activity.old_value === "" ? (
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}.</span>
|
||||
) : (
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}.</span>
|
||||
)}
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
|
||||
type TIssueStartDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueStartDateActivity: FC<TIssueStartDateActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<CalendarDays size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.new_value ? `set the start date to ` : `removed the start date `}
|
||||
{activity.new_value && (
|
||||
<>
|
||||
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||
</>
|
||||
)}
|
||||
{showIssue && (activity.new_value ? ` for ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
// icons
|
||||
import { DoubleCircleIcon } from "@plane/ui";
|
||||
|
||||
type TIssueStateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueStateActivity: FC<TIssueStateActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue ? ` for ` : ``}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
|
||||
type TIssueTargetDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
export const IssueTargetDateActivity: FC<TIssueTargetDateActivity> = observer((props) => {
|
||||
const { activityId, showIssue = true, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<CalendarDays size={14} color="#6b7280" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{activity.new_value ? `set the due date to ` : `removed the due date `}
|
||||
{activity.new_value && (
|
||||
<>
|
||||
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||
</>
|
||||
)}
|
||||
{showIssue && (activity.new_value ? ` for ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import {
|
||||
IssueDefaultActivity,
|
||||
IssueNameActivity,
|
||||
IssueDescriptionActivity,
|
||||
IssueStateActivity,
|
||||
IssueAssigneeActivity,
|
||||
IssuePriorityActivity,
|
||||
IssueEstimateActivity,
|
||||
IssueParentActivity,
|
||||
IssueRelationActivity,
|
||||
IssueStartDateActivity,
|
||||
IssueTargetDateActivity,
|
||||
IssueCycleActivity,
|
||||
IssueModuleActivity,
|
||||
IssueLabelActivity,
|
||||
IssueLinkActivity,
|
||||
IssueAttachmentActivity,
|
||||
IssueArchivedAtActivity,
|
||||
} from "./actions";
|
||||
|
||||
type TIssueActivityList = {
|
||||
activityId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
};
|
||||
|
||||
export const IssueActivityList: FC<TIssueActivityList> = observer((props) => {
|
||||
const { activityId, ends } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
comment: {},
|
||||
} = useIssueDetail();
|
||||
|
||||
const componentDefaultProps = { activityId, ends };
|
||||
|
||||
const activityField = getActivityById(activityId)?.field;
|
||||
switch (activityField) {
|
||||
case null: // default issue creation
|
||||
return <IssueDefaultActivity {...componentDefaultProps} />;
|
||||
case "state":
|
||||
return <IssueStateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "name":
|
||||
return <IssueNameActivity {...componentDefaultProps} />;
|
||||
case "description":
|
||||
return <IssueDescriptionActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "assignees":
|
||||
return <IssueAssigneeActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "priority":
|
||||
return <IssuePriorityActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "estimate_point":
|
||||
return <IssueEstimateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "parent":
|
||||
return <IssueParentActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case ["blocking", "blocked_by", "duplicate", "relates_to"].find((field) => field === activityField):
|
||||
return <IssueRelationActivity {...componentDefaultProps} />;
|
||||
case "start_date":
|
||||
return <IssueStartDateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "target_date":
|
||||
return <IssueTargetDateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "cycles":
|
||||
return <IssueCycleActivity {...componentDefaultProps} />;
|
||||
case "modules":
|
||||
return <IssueModuleActivity {...componentDefaultProps} />;
|
||||
case "labels":
|
||||
return <IssueLabelActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "link":
|
||||
return <IssueLinkActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "attachment":
|
||||
return <IssueAttachmentActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "archived_at":
|
||||
return <IssueArchivedAtActivity {...componentDefaultProps} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityList } from "./activity-list";
|
||||
|
||||
type TIssueActivityRoot = {
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueActivityRoot: FC<TIssueActivityRoot> = observer((props) => {
|
||||
const { issueId } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivitiesByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
const activityIds = getActivitiesByIssueId(issueId);
|
||||
|
||||
if (!activityIds) return <></>;
|
||||
return (
|
||||
<div>
|
||||
{activityIds.map((activityId, index) => (
|
||||
<IssueActivityList
|
||||
activityId={activityId}
|
||||
ends={index === 0 ? "top" : index === activityIds.length - 1 ? "bottom" : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { FC, ReactNode } from "react";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
|
||||
type TIssueCommentBlock = {
|
||||
commentId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
quickActions: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const IssueCommentBlock: FC<TIssueCommentBlock> = (props) => {
|
||||
const { commentId, ends, quickActions, children } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const comment = getCommentById(commentId);
|
||||
|
||||
if (!comment) return <></>;
|
||||
return (
|
||||
<div className={`relative flex gap-3 ${ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`}`}>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden={true} />
|
||||
<div className="flex-shrink-0 relative w-7 h-7 rounded-full flex justify-center items-center z-10 bg-gray-500 text-white border border-white uppercase font-medium">
|
||||
{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"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</>
|
||||
)}
|
||||
<div className="absolute top-2 left-4 w-5 h-5 rounded-full overflow-hidden flex justify-center items-center bg-custom-background-80">
|
||||
<MessageCircle className="w-3 h-3" color="#6b7280" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full relative flex ">
|
||||
<div className="w-full space-y-1">
|
||||
<div>
|
||||
<div className="text-xs capitalize">
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name + " Bot"
|
||||
: comment.actor_detail.display_name}
|
||||
</div>
|
||||
<div className="text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</div>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 ">{quickActions}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useMention, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueCommentBlock } from "./comment-block";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { IssueCommentReaction } from "../../reactions/issue-comment";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
import { TActivityOperations } from "../root";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
type TIssueCommentCard = {
|
||||
workspaceSlug: string;
|
||||
commentId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
const { workspaceSlug, commentId, activityOperations, ends, showAccessSpecifier = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
} = useIssueDetail();
|
||||
const { currentUser } = useUser();
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
const showEditorRef = useRef<any>(null);
|
||||
// state
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const comment = getCommentById(commentId);
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<Partial<TIssueComment>>({
|
||||
defaultValues: { comment_html: comment?.comment_html },
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<TIssueComment>) => {
|
||||
if (isSubmitting || !comment) return;
|
||||
setIsEditing(false);
|
||||
|
||||
activityOperations.updateComment(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment_html");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
if (!comment || !currentUser) return <></>;
|
||||
return (
|
||||
<IssueCommentBlock
|
||||
commentId={commentId}
|
||||
quickActions={
|
||||
<>
|
||||
{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={() => activityOperations.updateComment(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={() => activityOperations.updateComment(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={() => activityOperations.removeComment(comment.id)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(onEnter)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(comment?.workspace_detail?.slug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
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}
|
||||
/>
|
||||
|
||||
<IssueCommentReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={comment?.project_detail?.id}
|
||||
commentId={comment.id}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</IssueCommentBlock>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import { FC, useRef } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { Button } from "@plane/ui";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// types
|
||||
import { TActivityOperations } from "../root";
|
||||
import { TIssueComment } from "@plane/types";
|
||||
// icons
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
type TIssueCommentCreate = {
|
||||
workspaceSlug: string;
|
||||
activityOperations: TActivityOperations;
|
||||
disabled: boolean;
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
const { workspaceSlug, activityOperations, disabled, showAccessSpecifier = false } = props;
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
// react hook form
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
} = useForm<Partial<TIssueComment>>({ defaultValues: { comment_html: "<p></p>" } });
|
||||
|
||||
const onSubmit = async (formData: Partial<TIssueComment>) => {
|
||||
await activityOperations.createComment(formData).finally(() => {
|
||||
reset({ comment_html: "" });
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
ref={editorRef}
|
||||
value={!value ? "<p></p>" : value}
|
||||
customClassName="p-2"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(comment_json: Object, comment_html: string) => {
|
||||
onChange(comment_html);
|
||||
}}
|
||||
commentAccessSpecifier={
|
||||
showAccessSpecifier
|
||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||
: undefined
|
||||
}
|
||||
submitButton={
|
||||
<Button
|
||||
disabled={isSubmitting || disabled}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
onClick={(e) => {
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueCommentCard } from "./comment-card";
|
||||
// types
|
||||
import { TActivityOperations } from "../root";
|
||||
|
||||
type TIssueCommentRoot = {
|
||||
workspaceSlug: string;
|
||||
issueId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentsByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
const commentIds = getCommentsByIssueId(issueId);
|
||||
if (!commentIds) return <></>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{commentIds.map((commentId, index) => (
|
||||
<IssueCommentCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
commentId={commentId}
|
||||
ends={index === 0 ? "top" : index === commentIds.length - 1 ? "bottom" : undefined}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
12
web/components/issues/issue-detail/issue-activity/index.ts
Normal file
12
web/components/issues/issue-detail/issue-activity/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export * from "./root";
|
||||
|
||||
export * from "./activity-comment-root";
|
||||
|
||||
// activity
|
||||
export * from "./activity/root";
|
||||
export * from "./activity/activity-list";
|
||||
|
||||
// issue comment
|
||||
export * from "./comments/root";
|
||||
export * from "./comments/comment-card";
|
||||
export * from "./comments/comment-create";
|
||||
183
web/components/issues/issue-detail/issue-activity/root.tsx
Normal file
183
web/components/issues/issue-detail/issue-activity/root.tsx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { FC, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { History, LucideIcon, MessageCircle, ListRestart } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { IssueActivityCommentRoot, IssueActivityRoot, IssueCommentRoot, IssueCommentCreate } from "./";
|
||||
// types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
|
||||
type TIssueActivity = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type TActivityTabs = "all" | "activity" | "comments";
|
||||
|
||||
const activityTabs: { key: TActivityTabs; title: string; icon: LucideIcon }[] = [
|
||||
{
|
||||
key: "all",
|
||||
title: "All Activity",
|
||||
icon: History,
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
title: "Updates",
|
||||
icon: ListRestart,
|
||||
},
|
||||
{
|
||||
key: "comments",
|
||||
title: "Comments",
|
||||
icon: MessageCircle,
|
||||
},
|
||||
];
|
||||
|
||||
export type TActivityOperations = {
|
||||
createComment: (data: Partial<TIssueComment>) => Promise<void>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
removeComment: (commentId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled } = props;
|
||||
// hooks
|
||||
const { createComment, updateComment, removeComment } = useIssueDetail();
|
||||
const { setToastAlert } = useToast();
|
||||
const { getProjectById } = useProject();
|
||||
// state
|
||||
const [activityTab, setActivityTab] = useState<TActivityTabs>("all");
|
||||
|
||||
const activityOperations: TActivityOperations = useMemo(
|
||||
() => ({
|
||||
createComment: async (data: Partial<TIssueComment>) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await createComment(workspaceSlug, projectId, issueId, data);
|
||||
setToastAlert({
|
||||
title: "Comment created successfully.",
|
||||
type: "success",
|
||||
message: "Comment created successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment creation failed.",
|
||||
type: "error",
|
||||
message: "Comment creation failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
updateComment: async (commentId: string, data: Partial<TIssueComment>) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||
setToastAlert({
|
||||
title: "Comment updated successfully.",
|
||||
type: "success",
|
||||
message: "Comment updated successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment update failed.",
|
||||
type: "error",
|
||||
message: "Comment update failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
removeComment: async (commentId: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await removeComment(workspaceSlug, projectId, issueId, commentId);
|
||||
setToastAlert({
|
||||
title: "Comment removed successfully.",
|
||||
type: "success",
|
||||
message: "Comment removed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Comment remove failed.",
|
||||
type: "error",
|
||||
message: "Comment remove failed. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, removeComment, setToastAlert]
|
||||
);
|
||||
|
||||
const project = getProjectById(projectId);
|
||||
if (!project) return <></>;
|
||||
|
||||
return (
|
||||
<div className="space-y-3 pt-3">
|
||||
{/* header */}
|
||||
<div className="text-lg text-custom-text-100">Activity</div>
|
||||
|
||||
{/* rendering activity */}
|
||||
<div className="space-y-3">
|
||||
<div className="relative flex items-center gap-1">
|
||||
{activityTabs.map((tab) => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={`relative flex items-center px-2 py-1.5 gap-1 cursor-pointer transition-all rounded
|
||||
${
|
||||
tab.key === activityTab
|
||||
? `text-custom-text-100 bg-custom-background-80`
|
||||
: `text-custom-text-200 hover:bg-custom-background-80`
|
||||
}`}
|
||||
onClick={() => setActivityTab(tab.key)}
|
||||
>
|
||||
<div className="flex-shrink-0 w-4 h-4 flex justify-center items-center">
|
||||
<tab.icon className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="text-sm">{tab.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="min-h-[200px]">
|
||||
{activityTab === "all" ? (
|
||||
<div className="space-y-3">
|
||||
<IssueActivityCommentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
issueId={issueId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : activityTab === "activity" ? (
|
||||
<IssueActivityRoot issueId={issueId} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<IssueCommentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
issueId={issueId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
disabled={disabled}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
||||
import { IssueParentDetail } from "./parent";
|
||||
import { IssueReaction } from "./reactions";
|
||||
import { SubIssuesRoot } from "../sub-issues";
|
||||
import { IssueActivity } from "./issue-activity";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
|
|
@ -27,7 +28,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
// hooks
|
||||
const { currentUser } = useUser();
|
||||
const { getProjectById } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
|
|
@ -36,7 +36,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const projectDetails = projectId ? getProjectById(projectId) : null;
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||
|
||||
return (
|
||||
|
|
@ -94,7 +93,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* issue attachments */}
|
||||
<IssueAttachmentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
|
@ -102,20 +100,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
disabled={!is_editable}
|
||||
/>
|
||||
|
||||
{/* <div className="space-y-5 pt-3">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
||||
/>
|
||||
<AddComment
|
||||
onSubmit={handleAddComment}
|
||||
disabled={is_editable}
|
||||
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
||||
/>
|
||||
</div> */}
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
118
web/components/issues/issue-detail/reactions/issue-comment.tsx
Normal file
118
web/components/issues/issue-detail/reactions/issue-comment.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { ReactionSelector } from "./reaction-selector";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
export type TIssueCommentReaction = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
commentId: string;
|
||||
currentUser: IUser;
|
||||
};
|
||||
|
||||
export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, commentId, currentUser } = props;
|
||||
|
||||
// hooks
|
||||
const {
|
||||
commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser },
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
} = useIssueDetail();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const reactionIds = getCommentReactionsByCommentId(commentId);
|
||||
const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction);
|
||||
|
||||
const issueCommentReactionOperations = useMemo(
|
||||
() => ({
|
||||
create: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields");
|
||||
await createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
setToastAlert({
|
||||
title: "Reaction created successfully",
|
||||
type: "success",
|
||||
message: "Reaction created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Reaction creation failed",
|
||||
type: "error",
|
||||
message: "Reaction creation failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields");
|
||||
removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id);
|
||||
setToastAlert({
|
||||
title: "Reaction removed successfully",
|
||||
type: "success",
|
||||
message: "Reaction removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Reaction remove failed",
|
||||
type: "error",
|
||||
message: "Reaction remove failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
react: async (reaction: string) => {
|
||||
if (userReactions.includes(reaction)) await issueCommentReactionOperations.remove(reaction);
|
||||
else await issueCommentReactionOperations.create(reaction);
|
||||
},
|
||||
}),
|
||||
[
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
commentId,
|
||||
currentUser,
|
||||
createCommentReaction,
|
||||
removeCommentReaction,
|
||||
setToastAlert,
|
||||
userReactions,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 relative flex items-center gap-1.5">
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={userReactions}
|
||||
onSelect={issueCommentReactionOperations.react}
|
||||
/>
|
||||
|
||||
{reactionIds &&
|
||||
Object.keys(reactionIds || {}).map(
|
||||
(reaction) =>
|
||||
reactionIds[reaction]?.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => issueCommentReactionOperations.react(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||
{(reactionIds || {})[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -50,7 +50,7 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
|||
},
|
||||
remove: async (reaction: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields");
|
||||
await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id);
|
||||
setToastAlert({
|
||||
title: "Reaction removed successfully",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types";
|
|||
|
||||
export type TRelationObject = { name: string; icon: (size: number) => any; className: string };
|
||||
|
||||
const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
export const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
blocking: {
|
||||
name: "Blocking",
|
||||
icon: (size: number = 16) => <BlockerIcon height={size} width={size} />,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
|||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-4">
|
||||
{/* FIXME: --issue-detail-- */}
|
||||
{/* <ul role="list" className="-mb-4">
|
||||
{issueActivity ? (
|
||||
issueActivity.length > 0 &&
|
||||
issueActivity.map((activityId, index) => {
|
||||
|
|
@ -146,7 +147,7 @@ export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
|||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</ul>
|
||||
</ul> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import useToast from "hooks/use-toast";
|
|||
import {
|
||||
DeleteArchivedIssueModal,
|
||||
DeleteIssueModal,
|
||||
IssueActivity,
|
||||
IssueSubscription,
|
||||
IssueUpdateStatus,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewProperties,
|
||||
TIssueOperations,
|
||||
} from "components/issues";
|
||||
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||
// ui
|
||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -240,19 +240,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* <IssueActivity
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={currentUser}
|
||||
issueActivity={issueActivity}
|
||||
issueCommentCreate={issueCommentCreate}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
showCommentAccessSpecifier={showCommentAccessSpecifier}
|
||||
/> */}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex h-full w-full overflow-auto ${is_archived ? "opacity-60" : ""}`}>
|
||||
|
|
@ -269,19 +262,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{/* <IssueActivity
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={currentUser}
|
||||
issueActivity={issueActivity}
|
||||
issueCommentCreate={issueCommentCreate}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
showCommentAccessSpecifier={showCommentAccessSpecifier}
|
||||
/> */}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue