forked from github/plane
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
@ -469,19 +469,6 @@ class IssueReactionSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CommentReactionLiteSerializer(BaseSerializer):
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CommentReaction
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"reaction",
|
|
||||||
"comment",
|
|
||||||
"actor_detail",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CommentReactionSerializer(BaseSerializer):
|
class CommentReactionSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CommentReaction
|
model = CommentReaction
|
||||||
@ -512,7 +499,7 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
workspace_detail = WorkspaceLiteSerializer(
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
read_only=True, source="workspace"
|
read_only=True, source="workspace"
|
||||||
)
|
)
|
||||||
comment_reactions = CommentReactionLiteSerializer(
|
comment_reactions = CommentReactionSerializer(
|
||||||
read_only=True, many=True
|
read_only=True, many=True
|
||||||
)
|
)
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
|
@ -48,10 +48,8 @@ from plane.app.serializers import (
|
|||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
|
||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
RelatedIssueSerializer,
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -493,17 +491,27 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def get(self, request, slug, project_id, issue_id):
|
def get(self, request, slug, project_id, issue_id):
|
||||||
|
filters = {}
|
||||||
|
if request.GET.get("created_at__gt", None) is not None:
|
||||||
|
filters = {"created_at__gt": request.GET.get("created_at__gt")}
|
||||||
|
|
||||||
issue_activities = (
|
issue_activities = (
|
||||||
IssueActivity.objects.filter(issue_id=issue_id)
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
.filter(
|
.filter(
|
||||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
project__project_projectmember__member=self.request.user,
|
project__project_projectmember__member=self.request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
|
.filter(**filters)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
).order_by("created_at")
|
).order_by("created_at")
|
||||||
issue_comments = (
|
issue_comments = (
|
||||||
IssueComment.objects.filter(issue_id=issue_id)
|
IssueComment.objects.filter(issue_id=issue_id)
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
.order_by("created_at")
|
.order_by("created_at")
|
||||||
.select_related("actor", "issue", "project", "workspace")
|
.select_related("actor", "issue", "project", "workspace")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
@ -518,6 +526,12 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
).data
|
).data
|
||||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||||
|
|
||||||
|
if request.GET.get("activity_type", None) == "issue-property":
|
||||||
|
return Response(issue_activities, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
if request.GET.get("activity_type", None) == "issue-comment":
|
||||||
|
return Response(issue_comments, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
result_list = sorted(
|
result_list = sorted(
|
||||||
chain(issue_activities, issue_comments),
|
chain(issue_activities, issue_comments),
|
||||||
key=lambda instance: instance["created_at"],
|
key=lambda instance: instance["created_at"],
|
||||||
|
@ -111,15 +111,15 @@ def track_parent(
|
|||||||
issue_activities,
|
issue_activities,
|
||||||
epoch,
|
epoch,
|
||||||
):
|
):
|
||||||
if current_instance.get("parent") != requested_data.get("parent"):
|
if current_instance.get("parent_id") != requested_data.get("parent_id"):
|
||||||
old_parent = (
|
old_parent = (
|
||||||
Issue.objects.filter(pk=current_instance.get("parent")).first()
|
Issue.objects.filter(pk=current_instance.get("parent_id")).first()
|
||||||
if current_instance.get("parent") is not None
|
if current_instance.get("parent_id") is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
new_parent = (
|
new_parent = (
|
||||||
Issue.objects.filter(pk=requested_data.get("parent")).first()
|
Issue.objects.filter(pk=requested_data.get("parent_id")).first()
|
||||||
if requested_data.get("parent") is not None
|
if requested_data.get("parent_id") is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -188,9 +188,9 @@ def track_state(
|
|||||||
issue_activities,
|
issue_activities,
|
||||||
epoch,
|
epoch,
|
||||||
):
|
):
|
||||||
if current_instance.get("state") != requested_data.get("state"):
|
if current_instance.get("state_id") != requested_data.get("state_id"):
|
||||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
new_state = State.objects.get(pk=requested_data.get("state_id", None))
|
||||||
old_state = State.objects.get(pk=current_instance.get("state", None))
|
old_state = State.objects.get(pk=current_instance.get("state_id", None))
|
||||||
|
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -288,10 +288,10 @@ def track_labels(
|
|||||||
epoch,
|
epoch,
|
||||||
):
|
):
|
||||||
requested_labels = set(
|
requested_labels = set(
|
||||||
[str(lab) for lab in requested_data.get("labels", [])]
|
[str(lab) for lab in requested_data.get("label_ids", [])]
|
||||||
)
|
)
|
||||||
current_labels = set(
|
current_labels = set(
|
||||||
[str(lab) for lab in current_instance.get("labels", [])]
|
[str(lab) for lab in current_instance.get("label_ids", [])]
|
||||||
)
|
)
|
||||||
|
|
||||||
added_labels = requested_labels - current_labels
|
added_labels = requested_labels - current_labels
|
||||||
@ -350,10 +350,10 @@ def track_assignees(
|
|||||||
epoch,
|
epoch,
|
||||||
):
|
):
|
||||||
requested_assignees = set(
|
requested_assignees = set(
|
||||||
[str(asg) for asg in requested_data.get("assignees", [])]
|
[str(asg) for asg in requested_data.get("assignee_ids", [])]
|
||||||
)
|
)
|
||||||
current_assignees = set(
|
current_assignees = set(
|
||||||
[str(asg) for asg in current_instance.get("assignees", [])]
|
[str(asg) for asg in current_instance.get("assignee_ids", [])]
|
||||||
)
|
)
|
||||||
|
|
||||||
added_assignees = requested_assignees - current_assignees
|
added_assignees = requested_assignees - current_assignees
|
||||||
@ -541,14 +541,14 @@ def update_issue_activity(
|
|||||||
):
|
):
|
||||||
ISSUE_ACTIVITY_MAPPER = {
|
ISSUE_ACTIVITY_MAPPER = {
|
||||||
"name": track_name,
|
"name": track_name,
|
||||||
"parent": track_parent,
|
"parent_id": track_parent,
|
||||||
"priority": track_priority,
|
"priority": track_priority,
|
||||||
"state": track_state,
|
"state_id": track_state,
|
||||||
"description_html": track_description,
|
"description_html": track_description,
|
||||||
"target_date": track_target_date,
|
"target_date": track_target_date,
|
||||||
"start_date": track_start_date,
|
"start_date": track_start_date,
|
||||||
"labels": track_labels,
|
"label_ids": track_labels,
|
||||||
"assignees": track_assignees,
|
"assignee_ids": track_assignees,
|
||||||
"estimate_point": track_estimate_points,
|
"estimate_point": track_estimate_points,
|
||||||
"archived_at": track_archive_at,
|
"archived_at": track_archive_at,
|
||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
@ -1646,4 +1646,4 @@ def issue_activity(
|
|||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return
|
return
|
58
packages/types/src/issues/activity/base.d.ts
vendored
Normal file
58
packages/types/src/issues/activity/base.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
export * from "./issue_activity";
|
||||||
|
export * from "./issue_comment";
|
||||||
|
export * from "./issue_comment_reaction";
|
||||||
|
|
||||||
|
import { TIssuePriorities } from "../issues";
|
||||||
|
|
||||||
|
// root types
|
||||||
|
export type TIssueActivityWorkspaceDetail = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityProjectDetail = {
|
||||||
|
id: string;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
cover_image: string;
|
||||||
|
description: string | null;
|
||||||
|
emoji: string | null;
|
||||||
|
icon_prop: {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityIssueDetail = {
|
||||||
|
id: string;
|
||||||
|
sequence_id: boolean;
|
||||||
|
sort_order: boolean;
|
||||||
|
name: string;
|
||||||
|
description_html: string;
|
||||||
|
priority: TIssuePriorities;
|
||||||
|
start_date: string;
|
||||||
|
target_date: string;
|
||||||
|
is_draft: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityUserDetail = {
|
||||||
|
id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
avatar: string;
|
||||||
|
is_bot: boolean;
|
||||||
|
display_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityComment =
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
activity_type: "COMMENT";
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
activity_type: "ACTIVITY";
|
||||||
|
created_at?: string;
|
||||||
|
};
|
41
packages/types/src/issues/activity/issue_activity.d.ts
vendored
Normal file
41
packages/types/src/issues/activity/issue_activity.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
TIssueActivityWorkspaceDetail,
|
||||||
|
TIssueActivityProjectDetail,
|
||||||
|
TIssueActivityIssueDetail,
|
||||||
|
TIssueActivityUserDetail,
|
||||||
|
} from "./base";
|
||||||
|
|
||||||
|
export type TIssueActivity = {
|
||||||
|
id: string;
|
||||||
|
workspace: string;
|
||||||
|
workspace_detail: TIssueActivityWorkspaceDetail;
|
||||||
|
project: string;
|
||||||
|
project_detail: TIssueActivityProjectDetail;
|
||||||
|
issue: string;
|
||||||
|
issue_detail: TIssueActivityIssueDetail;
|
||||||
|
actor: string;
|
||||||
|
actor_detail: TIssueActivityUserDetail;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by: string | undefined;
|
||||||
|
updated_by: string | undefined;
|
||||||
|
attachments: any[];
|
||||||
|
|
||||||
|
verb: string;
|
||||||
|
field: string | undefined;
|
||||||
|
old_value: string | undefined;
|
||||||
|
new_value: string | undefined;
|
||||||
|
comment: string | undefined;
|
||||||
|
old_identifier: string | undefined;
|
||||||
|
new_identifier: string | undefined;
|
||||||
|
epoch: number;
|
||||||
|
issue_comment: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityMap = {
|
||||||
|
[issue_id: string]: TIssueActivity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueActivityIdMap = {
|
||||||
|
[issue_id: string]: string[];
|
||||||
|
};
|
39
packages/types/src/issues/activity/issue_comment.d.ts
vendored
Normal file
39
packages/types/src/issues/activity/issue_comment.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
TIssueActivityWorkspaceDetail,
|
||||||
|
TIssueActivityProjectDetail,
|
||||||
|
TIssueActivityIssueDetail,
|
||||||
|
TIssueActivityUserDetail,
|
||||||
|
} from "./base";
|
||||||
|
|
||||||
|
export type TIssueComment = {
|
||||||
|
id: string;
|
||||||
|
workspace: string;
|
||||||
|
workspace_detail: TIssueActivityWorkspaceDetail;
|
||||||
|
project: string;
|
||||||
|
project_detail: TIssueActivityProjectDetail;
|
||||||
|
issue: string;
|
||||||
|
issue_detail: TIssueActivityIssueDetail;
|
||||||
|
actor: string;
|
||||||
|
actor_detail: TIssueActivityUserDetail;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by: string | undefined;
|
||||||
|
updated_by: string | undefined;
|
||||||
|
attachments: any[];
|
||||||
|
|
||||||
|
comment_reactions: any[];
|
||||||
|
comment_stripped: string;
|
||||||
|
comment_html: string;
|
||||||
|
comment_json: object;
|
||||||
|
external_id: string | undefined;
|
||||||
|
external_source: string | undefined;
|
||||||
|
access: "EXTERNAL" | "INTERNAL";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueCommentMap = {
|
||||||
|
[issue_id: string]: TIssueComment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueCommentIdMap = {
|
||||||
|
[issue_id: string]: string[];
|
||||||
|
};
|
@ -1,20 +1,20 @@
|
|||||||
export type TIssueCommentReaction = {
|
export type TIssueCommentReaction = {
|
||||||
id: string;
|
id: string;
|
||||||
|
comment: string;
|
||||||
|
actor: string;
|
||||||
|
reaction: string;
|
||||||
|
workspace: string;
|
||||||
|
project: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
reaction: string;
|
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
project: string;
|
|
||||||
workspace: string;
|
|
||||||
actor: string;
|
|
||||||
comment: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueCommentReactionMap = {
|
export type TIssueCommentReactionMap = {
|
||||||
[issue_id: string]: TIssueCommentReaction;
|
[reaction_id: string]: TIssueCommentReaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueCommentReactionIdMap = {
|
export type TIssueCommentReactionIdMap = {
|
||||||
[issue_id: string]: string[];
|
[comment_id: string]: { [reaction: string]: string[] };
|
||||||
};
|
};
|
3
packages/types/src/issues/base.d.ts
vendored
3
packages/types/src/issues/base.d.ts
vendored
@ -4,9 +4,8 @@ export * from "./issue_reaction";
|
|||||||
export * from "./issue_link";
|
export * from "./issue_link";
|
||||||
export * from "./issue_attachment";
|
export * from "./issue_attachment";
|
||||||
export * from "./issue_relation";
|
export * from "./issue_relation";
|
||||||
export * from "./issue_activity";
|
|
||||||
export * from "./issue_comment_reaction";
|
|
||||||
export * from "./issue_sub_issues";
|
export * from "./issue_sub_issues";
|
||||||
|
export * from "./activity/base";
|
||||||
|
|
||||||
export type TLoader = "init-loader" | "mutation" | undefined;
|
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||||
|
|
||||||
|
41
packages/types/src/issues/issue_activity.d.ts
vendored
41
packages/types/src/issues/issue_activity.d.ts
vendored
@ -1,41 +0,0 @@
|
|||||||
export type TIssueActivity = {
|
|
||||||
access?: "EXTERNAL" | "INTERNAL";
|
|
||||||
actor: string;
|
|
||||||
actor_detail: IUserLite;
|
|
||||||
attachments: any[];
|
|
||||||
comment?: string;
|
|
||||||
comment_html?: string;
|
|
||||||
comment_stripped?: string;
|
|
||||||
created_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
field: string | null;
|
|
||||||
id: string;
|
|
||||||
issue: string | null;
|
|
||||||
issue_comment?: string | null;
|
|
||||||
issue_detail: {
|
|
||||||
description_html: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
priority: string | null;
|
|
||||||
sequence_id: string;
|
|
||||||
} | null;
|
|
||||||
new_identifier: string | null;
|
|
||||||
new_value: string | null;
|
|
||||||
old_identifier: string | null;
|
|
||||||
old_value: string | null;
|
|
||||||
project: string;
|
|
||||||
project_detail: IProjectLite;
|
|
||||||
updated_at: Date;
|
|
||||||
updated_by: string;
|
|
||||||
verb: string;
|
|
||||||
workspace: string;
|
|
||||||
workspace_detail?: IWorkspaceLite;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TIssueActivityMap = {
|
|
||||||
[issue_id: string]: TIssueActivity;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TIssueActivityIdMap = {
|
|
||||||
[issue_id: string]: string[];
|
|
||||||
};
|
|
@ -13,7 +13,7 @@ export type TIssueReaction = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueReactionMap = {
|
export type TIssueReactionMap = {
|
||||||
[issue_id: string]: TIssueReaction;
|
[reaction_id: string]: TIssueReaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueReactionIdMap = {
|
export type TIssueReactionIdMap = {
|
||||||
|
@ -187,109 +187,6 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
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: {
|
description: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
<>
|
<>
|
||||||
@ -448,6 +345,53 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
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: {
|
modules: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
@ -577,6 +521,77 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
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: {
|
start_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
if (!activity.new_value)
|
if (!activity.new_value)
|
||||||
@ -596,9 +611,7 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the start date to{" "}
|
set the start date to{" "}
|
||||||
<span className="font-medium text-custom-text-100">
|
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||||
{renderFormattedDate(activity.new_value)}
|
|
||||||
</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -611,21 +624,6 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
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: {
|
target_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
if (!activity.new_value)
|
if (!activity.new_value)
|
||||||
@ -645,9 +643,7 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the due date to{" "}
|
set the due date to{" "}
|
||||||
<span className="font-medium text-custom-text-100">
|
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
|
||||||
{renderFormattedDate(activity.new_value)}
|
|
||||||
</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
|
@ -88,41 +88,43 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
|
|||||||
const handleAddComment = async (formData: IIssueActivity) => {
|
const handleAddComment = async (formData: IIssueActivity) => {
|
||||||
if (!workspaceSlug || !issueDetails || !currentUser) return;
|
if (!workspaceSlug || !issueDetails || !currentUser) return;
|
||||||
|
|
||||||
await issueCommentService
|
/* FIXME: Replace this with the new issue activity component --issue-detail-- */
|
||||||
.createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
// await issueCommentService
|
||||||
.then((res) => {
|
// .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
// .then((res) => {
|
||||||
postHogEventTracker(
|
// mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||||
"COMMENT_ADDED",
|
// postHogEventTracker(
|
||||||
{
|
// "COMMENT_ADDED",
|
||||||
...res,
|
// {
|
||||||
state: "SUCCESS",
|
// ...res,
|
||||||
},
|
// state: "SUCCESS",
|
||||||
{
|
// },
|
||||||
isGrouping: true,
|
// {
|
||||||
groupType: "Workspace_metrics",
|
// isGrouping: true,
|
||||||
groupId: currentWorkspace?.id!,
|
// groupType: "Workspace_metrics",
|
||||||
}
|
// groupId: currentWorkspace?.id!,
|
||||||
);
|
// }
|
||||||
})
|
// );
|
||||||
.catch(() =>
|
// })
|
||||||
setToastAlert({
|
// .catch(() =>
|
||||||
type: "error",
|
// setToastAlert({
|
||||||
title: "Error!",
|
// type: "error",
|
||||||
message: "Comment could not be posted. Please try again.",
|
// title: "Error!",
|
||||||
})
|
// message: "Comment could not be posted. Please try again.",
|
||||||
);
|
// })
|
||||||
|
// );
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<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
|
<IssueActivitySection
|
||||||
activity={issueActivity}
|
activity={issueActivity}
|
||||||
handleCommentUpdate={handleCommentUpdate}
|
handleCommentUpdate={handleCommentUpdate}
|
||||||
handleCommentDelete={handleCommentDelete}
|
handleCommentDelete={handleCommentDelete}
|
||||||
/>
|
/>
|
||||||
<AddComment onSubmit={handleAddComment} />
|
<AddComment onSubmit={handleAddComment} /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -65,6 +65,7 @@ export const IssueActivitySection: React.FC<Props> = ({
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="relative flex items-start space-x-2">
|
<div className="relative flex items-start space-x-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="relative px-1.5">
|
<div className="relative px-1.5">
|
||||||
@ -97,6 +98,7 @@ export const IssueActivitySection: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="min-w-0 flex-1 py-3">
|
||||||
<div className="break-words text-xs text-custom-text-200">
|
<div className="break-words text-xs text-custom-text-200">
|
||||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
{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" />
|
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
@ -146,6 +147,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentUser?.id === comment.actor && (
|
{currentUser?.id === comment.actor && (
|
||||||
<CustomMenu ellipsis>
|
<CustomMenu ellipsis>
|
||||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||||
|
@ -69,7 +69,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue?.cycle_id}
|
value={issue?.cycle_id || undefined}
|
||||||
onChange={(value: any) => handleIssueCycleChange(value)}
|
onChange={(value: any) => handleIssueCycleChange(value)}
|
||||||
options={options}
|
options={options}
|
||||||
customButton={
|
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 { useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
||||||
import { IssueParentDetail } from "./parent";
|
import { IssueParentDetail } from "./parent";
|
||||||
import { IssueReaction } from "./reactions";
|
import { IssueReaction } from "./reactions";
|
||||||
import { SubIssuesRoot } from "../sub-issues";
|
import { SubIssuesRoot } from "../sub-issues";
|
||||||
|
import { IssueActivity } from "./issue-activity";
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -27,7 +28,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
// hooks
|
// hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const { getProjectById } = useProject();
|
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -36,7 +36,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
const projectDetails = projectId ? getProjectById(projectId) : null;
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -94,7 +93,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* issue attachments */}
|
|
||||||
<IssueAttachmentRoot
|
<IssueAttachmentRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -102,20 +100,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
disabled={!is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="space-y-5 pt-3">
|
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||||
<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> */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
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) => {
|
remove: async (reaction: string) => {
|
||||||
try {
|
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);
|
await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Reaction removed successfully",
|
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 };
|
export type TRelationObject = { name: string; icon: (size: number) => any; className: string };
|
||||||
|
|
||||||
const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
export const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
||||||
blocking: {
|
blocking: {
|
||||||
name: "Blocking",
|
name: "Blocking",
|
||||||
icon: (size: number = 16) => <BlockerIcon height={size} width={size} />,
|
icon: (size: number = 16) => <BlockerIcon height={size} width={size} />,
|
||||||
|
@ -41,7 +41,8 @@ export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root">
|
<div className="flow-root">
|
||||||
<ul role="list" className="-mb-4">
|
{/* FIXME: --issue-detail-- */}
|
||||||
|
{/* <ul role="list" className="-mb-4">
|
||||||
{issueActivity ? (
|
{issueActivity ? (
|
||||||
issueActivity.length > 0 &&
|
issueActivity.length > 0 &&
|
||||||
issueActivity.map((activityId, index) => {
|
issueActivity.map((activityId, index) => {
|
||||||
@ -146,7 +147,7 @@ export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
|||||||
<Loader.Item height="20px" />
|
<Loader.Item height="20px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,13 +12,13 @@ import useToast from "hooks/use-toast";
|
|||||||
import {
|
import {
|
||||||
DeleteArchivedIssueModal,
|
DeleteArchivedIssueModal,
|
||||||
DeleteIssueModal,
|
DeleteIssueModal,
|
||||||
IssueActivity,
|
|
||||||
IssueSubscription,
|
IssueSubscription,
|
||||||
IssueUpdateStatus,
|
IssueUpdateStatus,
|
||||||
PeekOverviewIssueDetails,
|
PeekOverviewIssueDetails,
|
||||||
PeekOverviewProperties,
|
PeekOverviewProperties,
|
||||||
TIssueOperations,
|
TIssueOperations,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
|
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||||
// ui
|
// ui
|
||||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -240,19 +240,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <IssueActivity
|
<IssueActivity
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
user={currentUser}
|
disabled={disabled}
|
||||||
issueActivity={issueActivity}
|
/>
|
||||||
issueCommentCreate={issueCommentCreate}
|
|
||||||
issueCommentUpdate={issueCommentUpdate}
|
|
||||||
issueCommentRemove={issueCommentRemove}
|
|
||||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
|
||||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
|
||||||
showCommentAccessSpecifier={showCommentAccessSpecifier}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`flex h-full w-full overflow-auto ${is_archived ? "opacity-60" : ""}`}>
|
<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)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <IssueActivity
|
<IssueActivity
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
user={currentUser}
|
disabled={disabled}
|
||||||
issueActivity={issueActivity}
|
/>
|
||||||
issueCommentCreate={issueCommentCreate}
|
|
||||||
issueCommentUpdate={issueCommentUpdate}
|
|
||||||
issueCommentRemove={issueCommentRemove}
|
|
||||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
|
||||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
|
||||||
showCommentAccessSpecifier={showCommentAccessSpecifier}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -4,5 +4,6 @@ export * from "./issue_draft.service";
|
|||||||
export * from "./issue_reaction.service";
|
export * from "./issue_reaction.service";
|
||||||
export * from "./issue_label.service";
|
export * from "./issue_label.service";
|
||||||
export * from "./issue_attachment.service";
|
export * from "./issue_attachment.service";
|
||||||
|
export * from "./issue_activity.service";
|
||||||
export * from "./issue_comment.service";
|
export * from "./issue_comment.service";
|
||||||
export * from "./issue_relation.service";
|
export * from "./issue_relation.service";
|
||||||
|
@ -3,11 +3,11 @@ import { APIService } from "services/api.service";
|
|||||||
// type
|
// type
|
||||||
import type {
|
import type {
|
||||||
TIssue,
|
TIssue,
|
||||||
IIssueActivity,
|
|
||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
ILinkDetails,
|
ILinkDetails,
|
||||||
TIssueLink,
|
TIssueLink,
|
||||||
TIssueSubIssues,
|
TIssueSubIssues,
|
||||||
|
TIssueActivity,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// helper
|
// helper
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
@ -59,7 +59,7 @@ export class IssueService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise<IIssueActivity[]> {
|
async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueActivity[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
33
web/services/issue/issue_activity.service.ts
Normal file
33
web/services/issue/issue_activity.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { APIService } from "services/api.service";
|
||||||
|
// types
|
||||||
|
import { TIssueActivity } from "@plane/types";
|
||||||
|
// helper
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
|
export class IssueActivityService extends APIService {
|
||||||
|
constructor() {
|
||||||
|
super(API_BASE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIssueActivities(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
params:
|
||||||
|
| {
|
||||||
|
created_at__gt: string;
|
||||||
|
}
|
||||||
|
| {} = {}
|
||||||
|
): Promise<TIssueActivity[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, {
|
||||||
|
params: {
|
||||||
|
activity_type: "issue-property",
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { APIService } from "services/api.service";
|
import { APIService } from "services/api.service";
|
||||||
// types
|
// types
|
||||||
import { IIssueActivity } from "@plane/types";
|
import { TIssueComment } from "@plane/types";
|
||||||
// helper
|
// helper
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
@ -9,8 +9,22 @@ export class IssueCommentService extends APIService {
|
|||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
async getIssueComments(
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`)
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
params:
|
||||||
|
| {
|
||||||
|
created_at__gt: string;
|
||||||
|
}
|
||||||
|
| {} = {}
|
||||||
|
): Promise<TIssueComment[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, {
|
||||||
|
params: {
|
||||||
|
activity_type: "issue-comment",
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
@ -21,7 +35,7 @@ export class IssueCommentService extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
data: Partial<IIssueActivity>
|
data: Partial<TIssueComment>
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
@ -35,7 +49,7 @@ export class IssueCommentService extends APIService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
commentId: string,
|
commentId: string,
|
||||||
data: Partial<IIssueActivity>
|
data: Partial<TIssueComment>
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.patch(
|
return this.patch(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`,
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`,
|
||||||
|
@ -271,6 +271,11 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
issueIds.map((issueId) => {
|
||||||
|
this.rootIssueStore.issues.updateIssue(issueId, { cycle_id: cycleId });
|
||||||
|
});
|
||||||
|
|
||||||
return issueToCycle;
|
return issueToCycle;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -1,57 +1,61 @@
|
|||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
|
import update from "lodash/update";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import uniq from "lodash/uniq";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueActivityService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { IIssueDetail } from "./root.store";
|
import { IIssueDetail } from "./root.store";
|
||||||
import { TIssueActivity, TIssueActivityIdMap, TIssueActivityMap } from "@plane/types";
|
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
|
||||||
|
|
||||||
|
export type TActivityLoader = "fetch" | "mutate" | undefined;
|
||||||
|
|
||||||
export interface IIssueActivityStoreActions {
|
export interface IIssueActivityStoreActions {
|
||||||
// actions
|
// actions
|
||||||
fetchActivities: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueActivity[]>;
|
fetchActivities: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
loaderType?: TActivityLoader
|
||||||
|
) => Promise<TIssueActivity[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueActivityStore extends IIssueActivityStoreActions {
|
export interface IIssueActivityStore extends IIssueActivityStoreActions {
|
||||||
// observables
|
// observables
|
||||||
|
loader: TActivityLoader;
|
||||||
activities: TIssueActivityIdMap;
|
activities: TIssueActivityIdMap;
|
||||||
activityMap: TIssueActivityMap;
|
activityMap: TIssueActivityMap;
|
||||||
// computed
|
|
||||||
issueActivities: string[] | undefined;
|
|
||||||
// helper methods
|
// helper methods
|
||||||
getActivitiesByIssueId: (issueId: string) => string[] | undefined;
|
getActivitiesByIssueId: (issueId: string) => string[] | undefined;
|
||||||
getActivityById: (activityId: string) => TIssueActivity | undefined;
|
getActivityById: (activityId: string) => TIssueActivity | undefined;
|
||||||
|
getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IssueActivityStore implements IIssueActivityStore {
|
export class IssueActivityStore implements IIssueActivityStore {
|
||||||
// observables
|
// observables
|
||||||
|
loader: TActivityLoader = "fetch";
|
||||||
activities: TIssueActivityIdMap = {};
|
activities: TIssueActivityIdMap = {};
|
||||||
activityMap: TIssueActivityMap = {};
|
activityMap: TIssueActivityMap = {};
|
||||||
// root store
|
// root store
|
||||||
rootIssueDetailStore: IIssueDetail;
|
rootIssueDetailStore: IIssueDetail;
|
||||||
// services
|
// services
|
||||||
issueService;
|
issueActivityService;
|
||||||
|
|
||||||
constructor(rootStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
|
loader: observable.ref,
|
||||||
activities: observable,
|
activities: observable,
|
||||||
activityMap: observable,
|
activityMap: observable,
|
||||||
// computed
|
|
||||||
issueActivities: computed,
|
|
||||||
// actions
|
// actions
|
||||||
fetchActivities: action,
|
fetchActivities: action,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
this.rootIssueDetailStore = rootStore;
|
this.rootIssueDetailStore = rootStore;
|
||||||
// services
|
// services
|
||||||
this.issueService = new IssueService();
|
this.issueActivityService = new IssueActivityService();
|
||||||
}
|
|
||||||
|
|
||||||
// computed
|
|
||||||
get issueActivities() {
|
|
||||||
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
|
|
||||||
if (!issueId) return undefined;
|
|
||||||
return this.activities[issueId] ?? undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper methods
|
// helper methods
|
||||||
@ -65,17 +69,73 @@ export class IssueActivityStore implements IIssueActivityStore {
|
|||||||
return this.activityMap[activityId] ?? undefined;
|
return this.activityMap[activityId] ?? undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getActivityCommentByIssueId = (issueId: string) => {
|
||||||
|
if (!issueId) return undefined;
|
||||||
|
|
||||||
|
let activityComments: TIssueActivityComment[] = [];
|
||||||
|
|
||||||
|
const activities = this.getActivitiesByIssueId(issueId) || [];
|
||||||
|
const comments = this.rootIssueDetailStore.comment.getCommentsByIssueId(issueId) || [];
|
||||||
|
|
||||||
|
activities.forEach((activityId) => {
|
||||||
|
const activity = this.getActivityById(activityId);
|
||||||
|
if (!activity) return;
|
||||||
|
activityComments.push({
|
||||||
|
id: activity.id,
|
||||||
|
activity_type: "ACTIVITY",
|
||||||
|
created_at: activity.created_at,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
comments.forEach((commentId) => {
|
||||||
|
const comment = this.rootIssueDetailStore.comment.getCommentById(commentId);
|
||||||
|
if (!comment) return;
|
||||||
|
activityComments.push({
|
||||||
|
id: comment.id,
|
||||||
|
activity_type: "COMMENT",
|
||||||
|
created_at: comment.created_at,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
activityComments = sortBy(activityComments, "created_at");
|
||||||
|
activityComments = activityComments.map((activityComment) => ({
|
||||||
|
id: activityComment.id,
|
||||||
|
activity_type: activityComment.activity_type,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return activityComments;
|
||||||
|
};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
fetchActivities = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetchActivities = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
loaderType: TActivityLoader = "fetch"
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const activities = await this.issueService.getIssueActivities(workspaceSlug, projectId, issueId);
|
this.loader = loaderType;
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
const _activityIds = this.getActivitiesByIssueId(issueId);
|
||||||
|
if (_activityIds && _activityIds.length > 0) {
|
||||||
|
const _activity = this.getActivityById(_activityIds[_activityIds.length - 1]);
|
||||||
|
if (_activity) props = { created_at__gt: _activity.created_at };
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = await this.issueActivityService.getIssueActivities(workspaceSlug, projectId, issueId, props);
|
||||||
|
|
||||||
const activityIds = activities.map((activity) => activity.id);
|
const activityIds = activities.map((activity) => activity.id);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.activities, issueId, activityIds);
|
update(this.activities, issueId, (_activityIds) => {
|
||||||
|
if (!_activityIds) return activityIds;
|
||||||
|
return uniq(concat(_activityIds, activityIds));
|
||||||
|
});
|
||||||
activities.forEach((activity) => {
|
activities.forEach((activity) => {
|
||||||
set(this.activityMap, activity.id, activity);
|
set(this.activityMap, activity.id, activity);
|
||||||
});
|
});
|
||||||
|
this.loader = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
return activities;
|
return activities;
|
||||||
|
@ -1,31 +1,55 @@
|
|||||||
import { action, makeObservable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import update from "lodash/update";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import uniq from "lodash/uniq";
|
||||||
|
import pull from "lodash/pull";
|
||||||
// services
|
// services
|
||||||
import { IssueCommentService } from "services/issue";
|
import { IssueCommentService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { IIssueDetail } from "./root.store";
|
import { IIssueDetail } from "./root.store";
|
||||||
import { TIssueActivity } from "@plane/types";
|
import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap } from "@plane/types";
|
||||||
|
|
||||||
|
export type TCommentLoader = "fetch" | "create" | "update" | "delete" | "mutate" | undefined;
|
||||||
|
|
||||||
export interface IIssueCommentStoreActions {
|
export interface IIssueCommentStoreActions {
|
||||||
|
fetchComments: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
loaderType?: TCommentLoader
|
||||||
|
) => Promise<TIssueComment[]>;
|
||||||
createComment: (
|
createComment: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
data: Partial<TIssueActivity>
|
data: Partial<TIssueComment>
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
updateComment: (
|
updateComment: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
commentId: string,
|
commentId: string,
|
||||||
data: Partial<TIssueActivity>
|
data: Partial<TIssueComment>
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
removeComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<any>;
|
removeComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueCommentStore extends IIssueCommentStoreActions {}
|
export interface IIssueCommentStore extends IIssueCommentStoreActions {
|
||||||
|
// observables
|
||||||
|
loader: TCommentLoader;
|
||||||
|
comments: TIssueCommentIdMap;
|
||||||
|
commentMap: TIssueCommentMap;
|
||||||
|
// helper methods
|
||||||
|
getCommentsByIssueId: (issueId: string) => string[] | undefined;
|
||||||
|
getCommentById: (activityId: string) => TIssueComment | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export class IssueCommentStore implements IIssueCommentStore {
|
export class IssueCommentStore implements IIssueCommentStore {
|
||||||
|
// observables
|
||||||
|
loader: TCommentLoader = "fetch";
|
||||||
|
comments: TIssueCommentIdMap = {};
|
||||||
|
commentMap: TIssueCommentMap = {};
|
||||||
// root store
|
// root store
|
||||||
rootIssueDetail: IIssueDetail;
|
rootIssueDetail: IIssueDetail;
|
||||||
// services
|
// services
|
||||||
@ -33,7 +57,12 @@ export class IssueCommentStore implements IIssueCommentStore {
|
|||||||
|
|
||||||
constructor(rootStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
|
// observables
|
||||||
|
loader: observable.ref,
|
||||||
|
comments: observable,
|
||||||
|
commentMap: observable,
|
||||||
// actions
|
// actions
|
||||||
|
fetchComments: action,
|
||||||
createComment: action,
|
createComment: action,
|
||||||
updateComment: action,
|
updateComment: action,
|
||||||
removeComment: action,
|
removeComment: action,
|
||||||
@ -44,13 +73,64 @@ export class IssueCommentStore implements IIssueCommentStore {
|
|||||||
this.issueCommentService = new IssueCommentService();
|
this.issueCommentService = new IssueCommentService();
|
||||||
}
|
}
|
||||||
|
|
||||||
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueActivity>) => {
|
// helper methods
|
||||||
|
getCommentsByIssueId = (issueId: string) => {
|
||||||
|
if (!issueId) return undefined;
|
||||||
|
return this.comments[issueId] ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
getCommentById = (commentId: string) => {
|
||||||
|
if (!commentId) return undefined;
|
||||||
|
return this.commentMap[commentId] ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchComments = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
loaderType: TCommentLoader = "fetch"
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
this.loader = loaderType;
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
const _commentIds = this.getCommentsByIssueId(issueId);
|
||||||
|
if (_commentIds && _commentIds.length > 0) {
|
||||||
|
const _comment = this.getCommentById(_commentIds[_commentIds.length - 1]);
|
||||||
|
if (_comment) props = { created_at__gt: _comment.created_at };
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await this.issueCommentService.getIssueComments(workspaceSlug, projectId, issueId, props);
|
||||||
|
|
||||||
|
const commentIds = comments.map((comment) => comment.id);
|
||||||
|
runInAction(() => {
|
||||||
|
update(this.comments, issueId, (_commentIds) => {
|
||||||
|
if (!_commentIds) return commentIds;
|
||||||
|
return uniq(concat(_commentIds, commentIds));
|
||||||
|
});
|
||||||
|
comments.forEach((comment) => {
|
||||||
|
this.rootIssueDetail.commentReaction.applyCommentReactions(comment.id, comment?.comment_reactions || []);
|
||||||
|
set(this.commentMap, comment.id, comment);
|
||||||
|
});
|
||||||
|
this.loader = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueComment>) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.issueCommentService.createIssueComment(workspaceSlug, projectId, issueId, data);
|
const response = await this.issueCommentService.createIssueComment(workspaceSlug, projectId, issueId, data);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.rootIssueDetail.activity.activities[issueId].push(response.id);
|
update(this.comments, issueId, (_commentIds) => {
|
||||||
set(this.rootIssueDetail.activity.activityMap, response.id, response);
|
if (!_commentIds) return [response.id];
|
||||||
|
return uniq(concat(_commentIds, [response.id]));
|
||||||
|
});
|
||||||
|
set(this.commentMap, response.id, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -64,12 +144,12 @@ export class IssueCommentStore implements IIssueCommentStore {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
commentId: string,
|
commentId: string,
|
||||||
data: Partial<TIssueActivity>
|
data: Partial<TIssueComment>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
set(this.rootIssueDetail.activity.activityMap, [commentId, key], data[key as keyof TIssueActivity]);
|
set(this.commentMap, [commentId, key], data[key as keyof TIssueComment]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,14 +172,10 @@ export class IssueCommentStore implements IIssueCommentStore {
|
|||||||
try {
|
try {
|
||||||
const response = await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId);
|
const response = await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId);
|
||||||
|
|
||||||
const reactionIndex = this.rootIssueDetail.activity.activities[issueId].findIndex(
|
runInAction(() => {
|
||||||
(_comment) => _comment === commentId
|
pull(this.comments[issueId], commentId);
|
||||||
);
|
delete this.commentMap[commentId];
|
||||||
if (reactionIndex >= 0)
|
});
|
||||||
runInAction(() => {
|
|
||||||
this.rootIssueDetail.activity.activities[issueId].splice(reactionIndex, 1);
|
|
||||||
delete this.rootIssueDetail.activity.activityMap[commentId];
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import update from "lodash/update";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import find from "lodash/find";
|
||||||
|
import pull from "lodash/pull";
|
||||||
// services
|
// services
|
||||||
import { IssueReactionService } from "services/issue";
|
import { IssueReactionService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { IIssueDetail } from "./root.store";
|
import { IIssueDetail } from "./root.store";
|
||||||
import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types";
|
import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types";
|
||||||
|
// helpers
|
||||||
|
import { groupReactions } from "helpers/emoji.helper";
|
||||||
|
|
||||||
export interface IIssueCommentReactionStoreActions {
|
export interface IIssueCommentReactionStoreActions {
|
||||||
// actions
|
// actions
|
||||||
@ -13,6 +19,7 @@ export interface IIssueCommentReactionStoreActions {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
commentId: string
|
commentId: string
|
||||||
) => Promise<TIssueCommentReaction[]>;
|
) => Promise<TIssueCommentReaction[]>;
|
||||||
|
applyCommentReactions: (commentId: string, commentReactions: TIssueCommentReaction[]) => void;
|
||||||
createCommentReaction: (
|
createCommentReaction: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -23,7 +30,8 @@ export interface IIssueCommentReactionStoreActions {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
commentId: string,
|
commentId: string,
|
||||||
reaction: string
|
reaction: string,
|
||||||
|
userId: string
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +40,9 @@ export interface IIssueCommentReactionStore extends IIssueCommentReactionStoreAc
|
|||||||
commentReactions: TIssueCommentReactionIdMap;
|
commentReactions: TIssueCommentReactionIdMap;
|
||||||
commentReactionMap: TIssueCommentReactionMap;
|
commentReactionMap: TIssueCommentReactionMap;
|
||||||
// helper methods
|
// helper methods
|
||||||
getCommentReactionsByCommentId: (commentId: string) => string[] | undefined;
|
getCommentReactionsByCommentId: (commentId: string) => { [reaction_id: string]: string[] } | undefined;
|
||||||
getCommentReactionById: (reactionId: string) => TIssueCommentReaction | undefined;
|
getCommentReactionById: (reactionId: string) => TIssueCommentReaction | undefined;
|
||||||
|
commentReactionsByUser: (commentId: string, userId: string) => TIssueCommentReaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
||||||
@ -52,6 +61,7 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
|||||||
commentReactionMap: observable,
|
commentReactionMap: observable,
|
||||||
// actions
|
// actions
|
||||||
fetchCommentReactions: action,
|
fetchCommentReactions: action,
|
||||||
|
applyCommentReactions: action,
|
||||||
createCommentReaction: action,
|
createCommentReaction: action,
|
||||||
removeCommentReaction: action,
|
removeCommentReaction: action,
|
||||||
});
|
});
|
||||||
@ -72,25 +82,67 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
|||||||
return this.commentReactionMap[reactionId] ?? undefined;
|
return this.commentReactionMap[reactionId] ?? undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
commentReactionsByUser = (commentId: string, userId: string) => {
|
||||||
|
if (!commentId || !userId) return [];
|
||||||
|
|
||||||
|
const reactions = this.getCommentReactionsByCommentId(commentId);
|
||||||
|
if (!reactions) return [];
|
||||||
|
|
||||||
|
const _userReactions: TIssueCommentReaction[] = [];
|
||||||
|
Object.keys(reactions).forEach((reaction) => {
|
||||||
|
if (reactions?.[reaction])
|
||||||
|
reactions?.[reaction].map((reactionId) => {
|
||||||
|
const currentReaction = this.getCommentReactionById(reactionId);
|
||||||
|
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return _userReactions;
|
||||||
|
};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => {
|
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => {
|
||||||
try {
|
try {
|
||||||
const reactions = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
|
const response = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
|
||||||
|
|
||||||
const reactionIds = reactions.map((reaction) => reaction.id);
|
const groupedReactions = groupReactions(response || [], "reaction");
|
||||||
runInAction(() => {
|
|
||||||
set(this.commentReactions, commentId, reactionIds);
|
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||||
reactions.forEach((reaction) => {
|
|
||||||
set(this.commentReactionMap, reaction.id, reaction);
|
Object.keys(groupedReactions).map((reactionId) => {
|
||||||
});
|
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||||
|
commentReactionIdsMap[reactionId] = reactionIds;
|
||||||
});
|
});
|
||||||
|
|
||||||
return reactions;
|
runInAction(() => {
|
||||||
|
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||||
|
response.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
applyCommentReactions = (commentId: string, commentReactions: TIssueCommentReaction[]) => {
|
||||||
|
const groupedReactions = groupReactions(commentReactions || [], "reaction");
|
||||||
|
|
||||||
|
const commentReactionIdsMap: { [reaction: string]: string[] } = {};
|
||||||
|
|
||||||
|
Object.keys(groupedReactions).map((reactionId) => {
|
||||||
|
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
|
||||||
|
commentReactionIdsMap[reactionId] = reactionIds;
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.commentReactions, commentId, commentReactionIdsMap);
|
||||||
|
commentReactions.forEach((reaction) => set(this.commentReactionMap, reaction.id, reaction));
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, {
|
const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, {
|
||||||
@ -98,7 +150,10 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.commentReactions[commentId].push(response.id);
|
update(this.commentReactions, [commentId, reaction], (reactionId) => {
|
||||||
|
if (!reactionId) return [response.id];
|
||||||
|
return concat(reactionId, response.id);
|
||||||
|
});
|
||||||
set(this.commentReactionMap, response.id, response);
|
set(this.commentReactionMap, response.id, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,14 +163,23 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
removeCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) => {
|
removeCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
reaction: string,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const reactionIndex = this.commentReactions[commentId].findIndex((_reaction) => _reaction === reaction);
|
const userReactions = this.commentReactionsByUser(commentId, userId);
|
||||||
if (reactionIndex >= 0)
|
const currentReaction = find(userReactions, { actor: userId, reaction: reaction });
|
||||||
|
|
||||||
|
if (currentReaction && currentReaction.id) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.commentReactions[commentId].splice(reactionIndex, 1);
|
pull(this.commentReactions[commentId][reaction], currentReaction.id);
|
||||||
delete this.commentReactionMap[reaction];
|
delete this.commentReactionMap[reaction];
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.issueReactionService.deleteIssueCommentReaction(
|
const response = await this.issueReactionService.deleteIssueCommentReaction(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
@ -77,6 +77,9 @@ export class IssueStore implements IIssueStore {
|
|||||||
// fetch issue activity
|
// fetch issue activity
|
||||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
// fetch issue comments
|
||||||
|
this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
// fetch issue subscription
|
// fetch issue subscription
|
||||||
this.rootIssueDetailStore.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
@ -92,36 +95,63 @@ export class IssueStore implements IIssueStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
const issue = await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
|
return issue;
|
||||||
|
};
|
||||||
|
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) =>
|
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
const cycle = await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
cycleId,
|
||||||
|
issueIds
|
||||||
|
);
|
||||||
|
if (issueIds && issueIds.length > 0)
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
||||||
|
return cycle;
|
||||||
|
};
|
||||||
|
|
||||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
this.rootIssueDetailStore.rootIssueStore.cycleIssues.removeIssueFromCycle(
|
const cycle = await this.rootIssueDetailStore.rootIssueStore.cycleIssues.removeIssueFromCycle(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
cycleId,
|
cycleId,
|
||||||
issueId
|
issueId
|
||||||
);
|
);
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
|
return cycle;
|
||||||
|
};
|
||||||
|
|
||||||
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) =>
|
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||||
this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule(
|
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
moduleId,
|
moduleId,
|
||||||
issueIds
|
issueIds
|
||||||
);
|
);
|
||||||
|
if (issueIds && issueIds.length > 0)
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]);
|
||||||
|
return _module;
|
||||||
|
};
|
||||||
|
|
||||||
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) =>
|
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||||
this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssueFromModule(
|
const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssueFromModule(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
moduleId,
|
moduleId,
|
||||||
issueId
|
issueId
|
||||||
);
|
);
|
||||||
|
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
|
return _module;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,8 @@ export class IssueLinkStore implements IIssueLinkStore {
|
|||||||
set(this.linkMap, response.id, response);
|
set(this.linkMap, response.id, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -123,6 +125,8 @@ export class IssueLinkStore implements IIssueLinkStore {
|
|||||||
|
|
||||||
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
|
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: fetch issue detail
|
// TODO: fetch issue detail
|
||||||
@ -141,6 +145,8 @@ export class IssueLinkStore implements IIssueLinkStore {
|
|||||||
delete this.linkMap[linkId];
|
delete this.linkMap[linkId];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -79,10 +79,11 @@ export class IssueReactionStore implements IIssueReactionStore {
|
|||||||
|
|
||||||
const _userReactions: TIssueReaction[] = [];
|
const _userReactions: TIssueReaction[] = [];
|
||||||
Object.keys(reactions).forEach((reaction) => {
|
Object.keys(reactions).forEach((reaction) => {
|
||||||
reactions[reaction].map((reactionId) => {
|
if (reactions?.[reaction])
|
||||||
const currentReaction = this.getReactionById(reactionId);
|
reactions?.[reaction].map((reactionId) => {
|
||||||
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
const currentReaction = this.getReactionById(reactionId);
|
||||||
});
|
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return _userReactions;
|
return _userReactions;
|
||||||
@ -126,6 +127,8 @@ export class IssueReactionStore implements IIssueReactionStore {
|
|||||||
set(this.reactionMap, response.id, response);
|
set(this.reactionMap, response.id, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -152,6 +155,8 @@ export class IssueReactionStore implements IIssueReactionStore {
|
|||||||
|
|
||||||
const response = await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction);
|
const response = await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction);
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -124,6 +124,8 @@ export class IssueRelationStore implements IIssueRelationStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -149,6 +151,8 @@ export class IssueRelationStore implements IIssueRelationStore {
|
|||||||
related_issue,
|
related_issue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetching activity
|
||||||
|
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchRelations(workspaceSlug, projectId, issueId);
|
this.fetchRelations(workspaceSlug, projectId, issueId);
|
||||||
|
@ -3,20 +3,20 @@ import { action, computed, makeObservable, observable } from "mobx";
|
|||||||
import { IIssueRootStore } from "../root.store";
|
import { IIssueRootStore } from "../root.store";
|
||||||
import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store";
|
import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store";
|
||||||
import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store";
|
import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store";
|
||||||
import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions } from "./activity.store";
|
|
||||||
import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions } from "./comment.store";
|
|
||||||
import {
|
|
||||||
IIssueCommentReactionStore,
|
|
||||||
IssueCommentReactionStore,
|
|
||||||
IIssueCommentReactionStoreActions,
|
|
||||||
} from "./comment_reaction.store";
|
|
||||||
import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store";
|
import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store";
|
||||||
import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store";
|
import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store";
|
||||||
import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store";
|
import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store";
|
||||||
import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store";
|
import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store";
|
||||||
import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store";
|
import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store";
|
||||||
|
import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store";
|
||||||
|
import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store";
|
||||||
|
import {
|
||||||
|
IIssueCommentReactionStore,
|
||||||
|
IssueCommentReactionStore,
|
||||||
|
IIssueCommentReactionStoreActions,
|
||||||
|
} from "./comment_reaction.store";
|
||||||
|
|
||||||
import { TIssue, IIssueActivity, TIssueLink, TIssueRelationTypes } from "@plane/types";
|
import { TIssue, TIssueComment, TIssueCommentReaction, TIssueLink, TIssueRelationTypes } from "@plane/types";
|
||||||
|
|
||||||
export type TPeekIssue = {
|
export type TPeekIssue = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -27,14 +27,14 @@ export type TPeekIssue = {
|
|||||||
export interface IIssueDetail
|
export interface IIssueDetail
|
||||||
extends IIssueStoreActions,
|
extends IIssueStoreActions,
|
||||||
IIssueReactionStoreActions,
|
IIssueReactionStoreActions,
|
||||||
IIssueActivityStoreActions,
|
|
||||||
IIssueCommentStoreActions,
|
|
||||||
IIssueCommentReactionStoreActions,
|
|
||||||
IIssueLinkStoreActions,
|
IIssueLinkStoreActions,
|
||||||
IIssueSubIssuesStoreActions,
|
IIssueSubIssuesStoreActions,
|
||||||
IIssueSubscriptionStoreActions,
|
IIssueSubscriptionStoreActions,
|
||||||
IIssueAttachmentStoreActions,
|
IIssueAttachmentStoreActions,
|
||||||
IIssueRelationStoreActions {
|
IIssueRelationStoreActions,
|
||||||
|
IIssueActivityStoreActions,
|
||||||
|
IIssueCommentStoreActions,
|
||||||
|
IIssueCommentReactionStoreActions {
|
||||||
// observables
|
// observables
|
||||||
peekIssue: TPeekIssue | undefined;
|
peekIssue: TPeekIssue | undefined;
|
||||||
isIssueLinkModalOpen: boolean;
|
isIssueLinkModalOpen: boolean;
|
||||||
@ -72,13 +72,13 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
issue: IIssueStore;
|
issue: IIssueStore;
|
||||||
reaction: IIssueReactionStore;
|
reaction: IIssueReactionStore;
|
||||||
attachment: IIssueAttachmentStore;
|
attachment: IIssueAttachmentStore;
|
||||||
activity: IIssueActivityStore;
|
|
||||||
comment: IIssueCommentStore;
|
|
||||||
commentReaction: IIssueCommentReactionStore;
|
|
||||||
subIssues: IIssueSubIssuesStore;
|
subIssues: IIssueSubIssuesStore;
|
||||||
link: IIssueLinkStore;
|
link: IIssueLinkStore;
|
||||||
subscription: IIssueSubscriptionStore;
|
subscription: IIssueSubscriptionStore;
|
||||||
relation: IIssueRelationStore;
|
relation: IIssueRelationStore;
|
||||||
|
activity: IIssueActivityStore;
|
||||||
|
comment: IIssueCommentStore;
|
||||||
|
commentReaction: IIssueCommentReactionStore;
|
||||||
|
|
||||||
constructor(rootStore: IIssueRootStore) {
|
constructor(rootStore: IIssueRootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -150,31 +150,6 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
userId: string
|
userId: string
|
||||||
) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId);
|
) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId);
|
||||||
|
|
||||||
// activity
|
|
||||||
fetchActivities = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
|
||||||
this.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
|
||||||
|
|
||||||
// comment
|
|
||||||
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssueActivity>) =>
|
|
||||||
this.comment.createComment(workspaceSlug, projectId, issueId, data);
|
|
||||||
updateComment = async (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
commentId: string,
|
|
||||||
data: Partial<IIssueActivity>
|
|
||||||
) => this.comment.updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
|
||||||
removeComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) =>
|
|
||||||
this.comment.removeComment(workspaceSlug, projectId, issueId, commentId);
|
|
||||||
|
|
||||||
// comment reaction
|
|
||||||
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) =>
|
|
||||||
this.commentReaction.fetchCommentReactions(workspaceSlug, projectId, commentId);
|
|
||||||
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
|
||||||
this.commentReaction.createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
|
||||||
removeCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
|
||||||
this.commentReaction.removeCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
|
||||||
|
|
||||||
// attachments
|
// attachments
|
||||||
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
this.attachment.fetchAttachments(workspaceSlug, projectId, issueId);
|
this.attachment.fetchAttachments(workspaceSlug, projectId, issueId);
|
||||||
@ -240,4 +215,38 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
relationType: TIssueRelationTypes,
|
relationType: TIssueRelationTypes,
|
||||||
relatedIssue: string
|
relatedIssue: string
|
||||||
) => this.relation.removeRelation(workspaceSlug, projectId, issueId, relationType, relatedIssue);
|
) => this.relation.removeRelation(workspaceSlug, projectId, issueId, relationType, relatedIssue);
|
||||||
|
|
||||||
|
// activity
|
||||||
|
fetchActivities = async (workspaceSlug: string, projectId: string, issueId: string, loaderType?: TActivityLoader) =>
|
||||||
|
this.activity.fetchActivities(workspaceSlug, projectId, issueId, loaderType);
|
||||||
|
|
||||||
|
// comment
|
||||||
|
fetchComments = async (workspaceSlug: string, projectId: string, issueId: string, loaderType?: TCommentLoader) =>
|
||||||
|
this.comment.fetchComments(workspaceSlug, projectId, issueId, loaderType);
|
||||||
|
createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueComment>) =>
|
||||||
|
this.comment.createComment(workspaceSlug, projectId, issueId, data);
|
||||||
|
updateComment = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
data: Partial<TIssueComment>
|
||||||
|
) => this.comment.updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||||
|
removeComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) =>
|
||||||
|
this.comment.removeComment(workspaceSlug, projectId, issueId, commentId);
|
||||||
|
|
||||||
|
// comment reaction
|
||||||
|
fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) =>
|
||||||
|
this.commentReaction.fetchCommentReactions(workspaceSlug, projectId, commentId);
|
||||||
|
applyCommentReactions = async (commentId: string, commentReactions: TIssueCommentReaction[]) =>
|
||||||
|
this.commentReaction.applyCommentReactions(commentId, commentReactions);
|
||||||
|
createCommentReaction = async (workspaceSlug: string, projectId: string, commentId: string, reaction: string) =>
|
||||||
|
this.commentReaction.createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||||
|
removeCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
reaction: string,
|
||||||
|
userId: string
|
||||||
|
) => this.commentReaction.removeCommentReaction(workspaceSlug, projectId, commentId, reaction, userId);
|
||||||
}
|
}
|
||||||
|
@ -264,6 +264,10 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
|||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
issueIds.map((issueId) => {
|
||||||
|
this.rootIssueStore.issues.updateIssue(issueId, { module_id: moduleId });
|
||||||
|
});
|
||||||
|
|
||||||
return issueToModule;
|
return issueToModule;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -146,7 +146,7 @@ export class ProjectPublishStore implements IProjectPublishStore {
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.projectPublishSettings = _projectPublishSettings;
|
this.projectPublishSettings = _projectPublishSettings;
|
||||||
set(this.projectRootStore.project.projectMap, [workspaceSlug, projectId, "is_deployed"], true);
|
set(this.projectRootStore.project.projectMap, [projectId, "is_deployed"], true);
|
||||||
this.generalLoader = false;
|
this.generalLoader = false;
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
@ -226,7 +226,7 @@ export class ProjectPublishStore implements IProjectPublishStore {
|
|||||||
);
|
);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.projectPublishSettings = "not-initialized";
|
this.projectPublishSettings = "not-initialized";
|
||||||
set(this.projectRootStore.project.projectMap, [workspaceSlug, projectId, "is_deployed"], false);
|
set(this.projectRootStore.project.projectMap, [projectId, "is_deployed"], false);
|
||||||
this.generalLoader = false;
|
this.generalLoader = false;
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
|
Loading…
Reference in New Issue
Block a user