diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 4f1767ccb..7bea75fa0 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -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 Meta: model = CommentReaction @@ -512,7 +499,7 @@ class IssueCommentSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" ) - comment_reactions = CommentReactionLiteSerializer( + comment_reactions = CommentReactionSerializer( read_only=True, many=True ) is_member = serializers.BooleanField(read_only=True) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 0d27b806d..2a0df13e5 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -48,10 +48,8 @@ from plane.app.serializers import ( ProjectMemberLiteSerializer, IssueReactionSerializer, CommentReactionSerializer, - IssueVoteSerializer, IssueRelationSerializer, RelatedIssueSerializer, - IssuePublicSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -493,17 +491,27 @@ class IssueActivityEndpoint(BaseAPIView): @method_decorator(gzip_page) 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 = ( IssueActivity.objects.filter(issue_id=issue_id) .filter( ~Q(field__in=["comment", "vote", "reaction", "draft"]), project__project_projectmember__member=self.request.user, + workspace__slug=slug, ) + .filter(**filters) .select_related("actor", "workspace", "issue", "project") ).order_by("created_at") issue_comments = ( 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") .select_related("actor", "issue", "project", "workspace") .prefetch_related( @@ -518,6 +526,12 @@ class IssueActivityEndpoint(BaseAPIView): ).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( chain(issue_activities, issue_comments), key=lambda instance: instance["created_at"], diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 686f06a20..06d3295dc 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -111,15 +111,15 @@ def track_parent( issue_activities, epoch, ): - if current_instance.get("parent") != requested_data.get("parent"): + if current_instance.get("parent_id") != requested_data.get("parent_id"): old_parent = ( - Issue.objects.filter(pk=current_instance.get("parent")).first() - if current_instance.get("parent") is not None + Issue.objects.filter(pk=current_instance.get("parent_id")).first() + if current_instance.get("parent_id") is not None else None ) new_parent = ( - Issue.objects.filter(pk=requested_data.get("parent")).first() - if requested_data.get("parent") is not None + Issue.objects.filter(pk=requested_data.get("parent_id")).first() + if requested_data.get("parent_id") is not None else None ) @@ -188,9 +188,9 @@ def track_state( issue_activities, epoch, ): - if current_instance.get("state") != requested_data.get("state"): - new_state = State.objects.get(pk=requested_data.get("state", None)) - old_state = State.objects.get(pk=current_instance.get("state", None)) + if current_instance.get("state_id") != requested_data.get("state_id"): + new_state = State.objects.get(pk=requested_data.get("state_id", None)) + old_state = State.objects.get(pk=current_instance.get("state_id", None)) issue_activities.append( IssueActivity( @@ -288,10 +288,10 @@ def track_labels( epoch, ): 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( - [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 @@ -350,10 +350,10 @@ def track_assignees( epoch, ): 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( - [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 @@ -541,14 +541,14 @@ def update_issue_activity( ): ISSUE_ACTIVITY_MAPPER = { "name": track_name, - "parent": track_parent, + "parent_id": track_parent, "priority": track_priority, - "state": track_state, + "state_id": track_state, "description_html": track_description, "target_date": track_target_date, "start_date": track_start_date, - "labels": track_labels, - "assignees": track_assignees, + "label_ids": track_labels, + "assignee_ids": track_assignees, "estimate_point": track_estimate_points, "archived_at": track_archive_at, "closed_to": track_closed_to, @@ -1646,4 +1646,4 @@ def issue_activity( if settings.DEBUG: print(e) capture_exception(e) - return + return \ No newline at end of file diff --git a/packages/types/src/issues/activity/base.d.ts b/packages/types/src/issues/activity/base.d.ts new file mode 100644 index 000000000..9f17d78c7 --- /dev/null +++ b/packages/types/src/issues/activity/base.d.ts @@ -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; + }; diff --git a/packages/types/src/issues/activity/issue_activity.d.ts b/packages/types/src/issues/activity/issue_activity.d.ts new file mode 100644 index 000000000..391d06c12 --- /dev/null +++ b/packages/types/src/issues/activity/issue_activity.d.ts @@ -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[]; +}; diff --git a/packages/types/src/issues/activity/issue_comment.d.ts b/packages/types/src/issues/activity/issue_comment.d.ts new file mode 100644 index 000000000..45d34be08 --- /dev/null +++ b/packages/types/src/issues/activity/issue_comment.d.ts @@ -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[]; +}; diff --git a/packages/types/src/issues/issue_comment_reaction.d.ts b/packages/types/src/issues/activity/issue_comment_reaction.d.ts similarity index 75% rename from packages/types/src/issues/issue_comment_reaction.d.ts rename to packages/types/src/issues/activity/issue_comment_reaction.d.ts index 8a3695e85..892a3e906 100644 --- a/packages/types/src/issues/issue_comment_reaction.d.ts +++ b/packages/types/src/issues/activity/issue_comment_reaction.d.ts @@ -1,20 +1,20 @@ export type TIssueCommentReaction = { id: string; + comment: string; + actor: string; + reaction: string; + workspace: string; + project: string; created_at: Date; updated_at: Date; - reaction: string; created_by: string; updated_by: string; - project: string; - workspace: string; - actor: string; - comment: string; }; export type TIssueCommentReactionMap = { - [issue_id: string]: TIssueCommentReaction; + [reaction_id: string]: TIssueCommentReaction; }; export type TIssueCommentReactionIdMap = { - [issue_id: string]: string[]; + [comment_id: string]: { [reaction: string]: string[] }; }; diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts index 08daceb16..ae210d3b1 100644 --- a/packages/types/src/issues/base.d.ts +++ b/packages/types/src/issues/base.d.ts @@ -4,9 +4,8 @@ export * from "./issue_reaction"; export * from "./issue_link"; export * from "./issue_attachment"; export * from "./issue_relation"; -export * from "./issue_activity"; -export * from "./issue_comment_reaction"; export * from "./issue_sub_issues"; +export * from "./activity/base"; export type TLoader = "init-loader" | "mutation" | undefined; diff --git a/packages/types/src/issues/issue_activity.d.ts b/packages/types/src/issues/issue_activity.d.ts deleted file mode 100644 index 2ce22b361..000000000 --- a/packages/types/src/issues/issue_activity.d.ts +++ /dev/null @@ -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[]; -}; diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts index 6fc071a9f..88ef27426 100644 --- a/packages/types/src/issues/issue_reaction.d.ts +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -13,7 +13,7 @@ export type TIssueReaction = { }; export type TIssueReactionMap = { - [issue_id: string]: TIssueReaction; + [reaction_id: string]: TIssueReaction; }; export type TIssueReactionIdMap = { diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index e49205459..792102be0 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -187,109 +187,6 @@ const activityDetails: { }, icon: , }, - blocking: { - message: (activity) => { - if (activity.old_value === "") - return ( - <> - marked this issue is blocking issue{" "} - {activity.new_value}. - > - ); - else - return ( - <> - removed the blocking issue {activity.old_value}. - > - ); - }, - icon: , - }, - blocked_by: { - message: (activity) => { - if (activity.old_value === "") - return ( - <> - marked this issue is being blocked by{" "} - {activity.new_value}. - > - ); - else - return ( - <> - removed this issue being blocked by issue{" "} - {activity.old_value}. - > - ); - }, - icon: , - }, - cycles: { - message: (activity, showIssue, workspaceSlug) => { - if (activity.verb === "created") - return ( - <> - added this issue to the cycle - - {activity.new_value} - - > - ); - else if (activity.verb === "updated") - return ( - <> - set the cycle to - - {activity.new_value} - - > - ); - else - return ( - <> - removed the issue from the cycle{" "} - - {activity.old_value} - - > - ); - }, - icon: , - }, - duplicate: { - message: (activity) => { - if (activity.old_value === "") - return ( - <> - marked this issue as duplicate of{" "} - {activity.new_value}. - > - ); - else - return ( - <> - removed this issue as a duplicate of{" "} - {activity.old_value}. - > - ); - }, - icon: , - }, description: { message: (activity, showIssue) => ( <> @@ -448,6 +345,53 @@ const activityDetails: { }, icon: , }, + cycles: { + message: (activity, showIssue, workspaceSlug) => { + if (activity.verb === "created") + return ( + <> + added this issue to the cycle + + {activity.new_value} + + > + ); + else if (activity.verb === "updated") + return ( + <> + set the cycle to + + {activity.new_value} + + > + ); + else + return ( + <> + removed the issue from the cycle{" "} + + {activity.old_value} + + > + ); + }, + icon: , + }, modules: { message: (activity, showIssue, workspaceSlug) => { if (activity.verb === "created") @@ -577,6 +521,77 @@ const activityDetails: { }, icon: , }, + blocking: { + message: (activity) => { + if (activity.old_value === "") + return ( + <> + marked this issue is blocking issue{" "} + {activity.new_value}. + > + ); + else + return ( + <> + removed the blocking issue {activity.old_value}. + > + ); + }, + icon: , + }, + blocked_by: { + message: (activity) => { + if (activity.old_value === "") + return ( + <> + marked this issue is being blocked by{" "} + {activity.new_value}. + > + ); + else + return ( + <> + removed this issue being blocked by issue{" "} + {activity.old_value}. + > + ); + }, + icon: , + }, + duplicate: { + message: (activity) => { + if (activity.old_value === "") + return ( + <> + marked this issue as duplicate of{" "} + {activity.new_value}. + > + ); + else + return ( + <> + removed this issue as a duplicate of{" "} + {activity.old_value}. + > + ); + }, + icon: , + }, + state: { + message: (activity, showIssue) => ( + <> + set the state to {activity.new_value} + {showIssue && ( + <> + {" "} + for + > + )} + . + > + ), + icon: , + }, start_date: { message: (activity, showIssue) => { if (!activity.new_value) @@ -596,9 +611,7 @@ const activityDetails: { return ( <> set the start date to{" "} - - {renderFormattedDate(activity.new_value)} - + {renderFormattedDate(activity.new_value)} {showIssue && ( <> {" "} @@ -611,21 +624,6 @@ const activityDetails: { }, icon: , }, - state: { - message: (activity, showIssue) => ( - <> - set the state to {activity.new_value} - {showIssue && ( - <> - {" "} - for - > - )} - . - > - ), - icon: , - }, target_date: { message: (activity, showIssue) => { if (!activity.new_value) @@ -645,9 +643,7 @@ const activityDetails: { return ( <> set the due date to{" "} - - {renderFormattedDate(activity.new_value)} - + {renderFormattedDate(activity.new_value)} {showIssue && ( <> {" "} diff --git a/web/components/inbox/issue-activity.tsx b/web/components/inbox/issue-activity.tsx index e079fd8d8..a3b3978e5 100644 --- a/web/components/inbox/issue-activity.tsx +++ b/web/components/inbox/issue-activity.tsx @@ -88,41 +88,43 @@ export const InboxIssueActivity: React.FC = observer(({ issueDetails }) = const handleAddComment = async (formData: IIssueActivity) => { if (!workspaceSlug || !issueDetails || !currentUser) return; - await issueCommentService - .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData) - .then((res) => { - mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id)); - postHogEventTracker( - "COMMENT_ADDED", - { - ...res, - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Comment could not be posted. Please try again.", - }) - ); + /* FIXME: Replace this with the new issue activity component --issue-detail-- */ + // await issueCommentService + // .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData) + // .then((res) => { + // mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id)); + // postHogEventTracker( + // "COMMENT_ADDED", + // { + // ...res, + // state: "SUCCESS", + // }, + // { + // isGrouping: true, + // groupType: "Workspace_metrics", + // groupId: currentWorkspace?.id!, + // } + // ); + // }) + // .catch(() => + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Comment could not be posted. Please try again.", + // }) + // ); }; return ( - Comments/Activity + {/* FIXME: Replace this with the new issue activity component --issue-detail-- */} + {/* Comments/Activity - + */} ); }); diff --git a/web/components/issues/activity.tsx b/web/components/issues/activity.tsx index bb09caf6a..6dae8486f 100644 --- a/web/components/issues/activity.tsx +++ b/web/components/issues/activity.tsx @@ -65,6 +65,7 @@ export const IssueActivitySection: React.FC = ({ aria-hidden="true" /> ) : null} + @@ -97,6 +98,7 @@ export const IssueActivitySection: React.FC = ({ + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index 875280968..05c7081ec 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -87,6 +87,7 @@ export const CommentCard: React.FC = observer((props) => { + @@ -146,6 +147,7 @@ export const CommentCard: React.FC = observer((props) => { + {currentUser?.id === comment.actor && ( setIsEditing(true)} className="flex items-center gap-1"> diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 24ed1c963..5185ac790 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -69,7 +69,7 @@ export const IssueCycleSelect: React.FC = observer((props) => return ( handleIssueCycleChange(value)} options={options} customButton={ diff --git a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx new file mode 100644 index 000000000..575e8d841 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -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 = 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 ( + + {activityComments.map((activityComment, index) => + activityComment.activity_type === "COMMENT" ? ( + + ) : activityComment.activity_type === "ACTIVITY" ? ( + + ) : ( + <>> + ) + )} + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx new file mode 100644 index 000000000..55f07870c --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + {activity.new_value === "restore" ? `restored the issue` : `archived the issue`}. + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx new file mode 100644 index 000000000..449297cbe --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/assignee.tsx @@ -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 = observer((props) => { + const { activityId, ends, showIssue = true } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.old_value === "" ? `added a new assignee ` : `removed the assignee `} + + + {activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value} + + + {showIssue && (activity.old_value === "" ? ` to ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx new file mode 100644 index 000000000..d9b4475c5 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.verb === "created" ? `uploaded a new ` : `removed an attachment`} + {activity.verb === "created" && ( + + attachment + + )} + {showIssue && (activity.verb === "created" ? ` to ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx new file mode 100644 index 000000000..8336e516f --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.verb === "created" ? ( + <> + added this issue to the cycle + + {activity.new_value} + + > + ) : activity.verb === "updated" ? ( + <> + set the cycle to + + {activity.new_value} + + > + ) : ( + <> + removed the issue from the cycle + + {activity.new_value} + + > + )} + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx new file mode 100644 index 000000000..e45387535 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + ends={ends} + > + <>{activity.verb === "created" ? " created the issue." : " deleted an issue."}> + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/description.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/description.tsx new file mode 100644 index 000000000..30f445ec0 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/description.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + updated the description + {showIssue ? ` of ` : ``} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx new file mode 100644 index 000000000..f43b92f16 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx @@ -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 = 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 ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.new_value ? `set the estimate point to ` : `removed the estimate point `} + {activity.new_value && ( + <> + + + {areEstimatesEnabledForCurrentProject + ? estimateValue + : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} + + + > + )} + {showIssue && (activity.new_value ? ` to ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx new file mode 100644 index 000000000..eabe5d518 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -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 = (props) => { + const { icon, activityId, ends, children } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + + + + {icon ? icon : } + + + + {children} + + + {calculateTimeAgo(activity.created_at)} + + + + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx new file mode 100644 index 000000000..e86b1fb57 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -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 = (props) => { + const { activityId } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + + + {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}{" "} + {activity.issue_detail?.name} + + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-user.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-user.tsx new file mode 100644 index 000000000..dd44879cf --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-user.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; + +// hooks +import { useIssueDetail } from "hooks/store"; +// ui + +type TIssueUser = { + activityId: string; +}; + +export const IssueUser: FC = (props) => { + const { activityId } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + + {activity.actor_detail?.display_name} + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts b/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts new file mode 100644 index 000000000..02108d70b --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/index.ts @@ -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"; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/label.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/label.tsx new file mode 100644 index 000000000..b9c59c9b3 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/label.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + const { projectLabels } = useLabel(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.old_value === "" ? `added a new label ` : `removed the label `} + {activity.old_value === "" ? ( + + l.id === activity.new_identifier)?.color ?? "#000000", + }} + aria-hidden="true" + /> + {activity.new_value} + + ) : ( + + l.id === activity.old_identifier)?.color ?? "#000000", + }} + aria-hidden="true" + /> + {activity.old_value} + + )} + {showIssue && (activity.old_value === "" ? ` to ` : ` from `)} + {showIssue && } + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/link.tsx new file mode 100644 index 000000000..15343392f --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/link.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = false, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.verb === "created" ? ( + <> + added this + + link + + > + ) : activity.verb === "updated" ? ( + <> + updated the + + link + + > + ) : ( + <> + removed this + + link + + > + )} + {showIssue && (activity.verb === "created" ? ` to ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx new file mode 100644 index 000000000..e092ab08b --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.verb === "created" ? ( + <> + added this issue to the module + + {activity.new_value} + + > + ) : activity.verb === "updated" ? ( + <> + set the module to + + {activity.new_value} + + > + ) : ( + <> + removed the issue from the module + + {activity.new_value} + + > + )} + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/name.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/name.tsx new file mode 100644 index 000000000..7a78be7bd --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/name.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <>set the name to {activity.new_value}.> + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/parent.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/parent.tsx new file mode 100644 index 000000000..afe814ee2 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/parent.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.new_value ? `set the parent to ` : `removed the parent `} + {activity.new_value ? ( + {activity.new_value} + ) : ( + {activity.old_value} + )} + {showIssue && (activity.new_value ? ` for ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/priority.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/priority.tsx new file mode 100644 index 000000000..273bd319b --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/priority.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + set the priority to {activity.new_value} + {showIssue ? ` for ` : ``} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx new file mode 100644 index 000000000..e68a7c373 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx @@ -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 = observer((props) => { + const { activityId, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + >} + 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 === "" ? ( + {activity.new_value}. + ) : ( + {activity.old_value}. + )} + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx new file mode 100644 index 000000000..95b3cda80 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.new_value ? `set the start date to ` : `removed the start date `} + {activity.new_value && ( + <> + {renderFormattedDate(activity.new_value)} + > + )} + {showIssue && (activity.new_value ? ` for ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx new file mode 100644 index 000000000..7cc47c2c8 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + set the state to {activity.new_value} + {showIssue ? ` for ` : ``} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx new file mode 100644 index 000000000..a4b40ec31 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx @@ -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 = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>>; + return ( + } + activityId={activityId} + ends={ends} + > + <> + {activity.new_value ? `set the due date to ` : `removed the due date `} + {activity.new_value && ( + <> + {renderFormattedDate(activity.new_value)} + > + )} + {showIssue && (activity.new_value ? ` for ` : ` from `)} + {showIssue && }. + > + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx new file mode 100644 index 000000000..0f5f6876e --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/activity-list.tsx @@ -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 = 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 ; + case "state": + return ; + case "name": + return ; + case "description": + return ; + case "assignees": + return ; + case "priority": + return ; + case "estimate_point": + return ; + case "parent": + return ; + case ["blocking", "blocked_by", "duplicate", "relates_to"].find((field) => field === activityField): + return ; + case "start_date": + return ; + case "target_date": + return ; + case "cycles": + return ; + case "modules": + return ; + case "labels": + return ; + case "link": + return ; + case "attachment": + return ; + case "archived_at": + return ; + default: + return <>>; + } +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/root.tsx b/web/components/issues/issue-detail/issue-activity/activity/root.tsx new file mode 100644 index 000000000..af44463d5 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/activity/root.tsx @@ -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 = observer((props) => { + const { issueId } = props; + // hooks + const { + activity: { getActivitiesByIssueId }, + } = useIssueDetail(); + + const activityIds = getActivitiesByIssueId(issueId); + + if (!activityIds) return <>>; + return ( + + {activityIds.map((activityId, index) => ( + + ))} + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx new file mode 100644 index 000000000..4dbc36f6b --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx @@ -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 = (props) => { + const { commentId, ends, quickActions, children } = props; + // hooks + const { + comment: { getCommentById }, + } = useIssueDetail(); + + const comment = getCommentById(commentId); + + if (!comment) return <>>; + return ( + + + + {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( + + ) : ( + <> + {comment.actor_detail.is_bot + ? comment.actor_detail.first_name.charAt(0) + : comment.actor_detail.display_name.charAt(0)} + > + )} + + + + + + + + + {comment.actor_detail.is_bot + ? comment.actor_detail.first_name + " Bot" + : comment.actor_detail.display_name} + + commented {calculateTimeAgo(comment.created_at)} + + {children} + + {quickActions} + + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx new file mode 100644 index 000000000..38110c842 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx @@ -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 = (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(null); + const showEditorRef = useRef(null); + // state + const [isEditing, setIsEditing] = useState(false); + + const comment = getCommentById(commentId); + + const { + formState: { isSubmitting }, + handleSubmit, + setFocus, + watch, + setValue, + } = useForm>({ + defaultValues: { comment_html: comment?.comment_html }, + }); + + const onEnter = (formData: Partial) => { + 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 ( + + {currentUser?.id === comment.actor && ( + + setIsEditing(true)} className="flex items-center gap-1"> + + Edit comment + + {showAccessSpecifier && ( + <> + {comment.access === "INTERNAL" ? ( + activityOperations.updateComment(comment.id, { access: "EXTERNAL" })} + className="flex items-center gap-1" + > + + Switch to public comment + + ) : ( + activityOperations.updateComment(comment.id, { access: "INTERNAL" })} + className="flex items-center gap-1" + > + + Switch to private comment + + )} + > + )} + activityOperations.removeComment(comment.id)} + className="flex items-center gap-1" + > + + Delete comment + + + )} + > + } + ends={ends} + > + <> + + + setValue("comment_html", comment_html)} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + + + + + + setIsEditing(false)} + > + + + + + + {showAccessSpecifier && ( + + {comment.access === "INTERNAL" ? : } + + )} + + + + + > + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx new file mode 100644 index 000000000..af439e9f6 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -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 = (props) => { + const { workspaceSlug, activityOperations, disabled, showAccessSpecifier = false } = props; + // refs + const editorRef = useRef(null); + // react hook form + const { + handleSubmit, + control, + formState: { isSubmitting }, + reset, + } = useForm>({ defaultValues: { comment_html: "" } }); + + const onSubmit = async (formData: Partial) => { + await activityOperations.createComment(formData).finally(() => { + reset({ comment_html: "" }); + editorRef.current?.clearEditor(); + }); + }; + + return ( + + ( + ( + { + handleSubmit(onSubmit)(e); + }} + cancelUploadImage={fileService.cancelUpload} + uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} + deleteFile={fileService.deleteImage} + restoreFile={fileService.restoreImage} + ref={editorRef} + value={!value ? "" : 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={ + { + handleSubmit(onSubmit)(e); + }} + > + {isSubmitting ? "Adding..." : "Comment"} + + } + /> + )} + /> + )} + /> + + ); +}; diff --git a/web/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/components/issues/issue-detail/issue-activity/comments/root.tsx new file mode 100644 index 000000000..4e2775c4a --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -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 = observer((props) => { + const { workspaceSlug, issueId, activityOperations, showAccessSpecifier } = props; + // hooks + const { + comment: { getCommentsByIssueId }, + } = useIssueDetail(); + + const commentIds = getCommentsByIssueId(issueId); + if (!commentIds) return <>>; + + return ( + + {commentIds.map((commentId, index) => ( + + ))} + + ); +}); diff --git a/web/components/issues/issue-detail/issue-activity/index.ts b/web/components/issues/issue-detail/issue-activity/index.ts new file mode 100644 index 000000000..5c6634ce5 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/index.ts @@ -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"; diff --git a/web/components/issues/issue-detail/issue-activity/root.tsx b/web/components/issues/issue-detail/issue-activity/root.tsx new file mode 100644 index 000000000..67d0a48f8 --- /dev/null +++ b/web/components/issues/issue-detail/issue-activity/root.tsx @@ -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) => Promise; + updateComment: (commentId: string, data: Partial) => Promise; + removeComment: (commentId: string) => Promise; +}; + +export const IssueActivity: FC = 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("all"); + + const activityOperations: TActivityOperations = useMemo( + () => ({ + createComment: async (data: Partial) => { + 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) => { + 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 ( + + {/* header */} + Activity + + {/* rendering activity */} + + + {activityTabs.map((tab) => ( + setActivityTab(tab.key)} + > + + + + {tab.title} + + ))} + + + + {activityTab === "all" ? ( + + + {!disabled && ( + + )} + + ) : activityTab === "activity" ? ( + + ) : ( + + + {!disabled && ( + + )} + + )} + + + + ); +}); diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index fcbe54a1c..f9ac5fe53 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,12 +1,13 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; +import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // components import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; import { SubIssuesRoot } from "../sub-issues"; +import { IssueActivity } from "./issue-activity"; // ui import { StateGroupIcon } from "@plane/ui"; // types @@ -27,7 +28,6 @@ export const IssueMainContent: React.FC = observer((props) => { const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); // hooks const { currentUser } = useUser(); - const { getProjectById } = useProject(); const { projectStates } = useProjectState(); const { issue: { getIssueById }, @@ -36,7 +36,6 @@ export const IssueMainContent: React.FC = observer((props) => { const issue = getIssueById(issueId); if (!issue) return <>>; - const projectDetails = projectId ? getProjectById(projectId) : null; const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); return ( @@ -94,7 +93,6 @@ export const IssueMainContent: React.FC = observer((props) => { )} - {/* issue attachments */} = observer((props) => { disabled={!is_editable} /> - {/* - Comments/Activity - - - */} + > ); }); diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx new file mode 100644 index 000000000..30a8621e4 --- /dev/null +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -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 = 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 ( + + + + {reactionIds && + Object.keys(reactionIds || {}).map( + (reaction) => + reactionIds[reaction]?.length > 0 && ( + <> + 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" + }`} + > + {renderEmoji(reaction)} + + {(reactionIds || {})[reaction].length}{" "} + + + > + ) + )} + + ); +}); diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index 1627a6730..d6b33e36b 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -50,7 +50,7 @@ export const IssueReaction: FC = observer((props) => { }, remove: async (reaction: string) => { try { - if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields"); await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id); setToastAlert({ title: "Reaction removed successfully", diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 30a81f2dd..ac3d54785 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -13,7 +13,7 @@ import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types"; export type TRelationObject = { name: string; icon: (size: number) => any; className: string }; -const issueRelationObject: Record = { +export const issueRelationObject: Record = { blocking: { name: "Blocking", icon: (size: number = 16) => , diff --git a/web/components/issues/peek-overview/activity/card.tsx b/web/components/issues/peek-overview/activity/card.tsx index e1b2a3cc3..42408f48b 100644 --- a/web/components/issues/peek-overview/activity/card.tsx +++ b/web/components/issues/peek-overview/activity/card.tsx @@ -41,7 +41,8 @@ export const IssueActivityCard: FC = (props) => { return ( - + {/* FIXME: --issue-detail-- */} + {/* {issueActivity ? ( issueActivity.length > 0 && issueActivity.map((activityId, index) => { @@ -146,7 +147,7 @@ export const IssueActivityCard: FC = (props) => { )} - + */} ); }; diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index a08671455..adbd9c940 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -12,13 +12,13 @@ import useToast from "hooks/use-toast"; import { DeleteArchivedIssueModal, DeleteIssueModal, - IssueActivity, IssueSubscription, IssueUpdateStatus, PeekOverviewIssueDetails, PeekOverviewProperties, TIssueOperations, } from "components/issues"; +import { IssueActivity } from "../issue-detail/issue-activity"; // ui import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; // helpers @@ -240,19 +240,12 @@ export const IssueView: FC = observer((props) => { disabled={disabled} /> - {/* */} + disabled={disabled} + /> ) : ( @@ -269,19 +262,12 @@ export const IssueView: FC = observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> - {/* */} + disabled={disabled} + /> { + async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`) .then((response) => response?.data) .catch((error) => { diff --git a/web/services/issue/issue_activity.service.ts b/web/services/issue/issue_activity.service.ts new file mode 100644 index 000000000..87c7a8f54 --- /dev/null +++ b/web/services/issue/issue_activity.service.ts @@ -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 { + 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; + }); + } +} diff --git a/web/services/issue/issue_comment.service.ts b/web/services/issue/issue_comment.service.ts index e36bc3500..8001d644a 100644 --- a/web/services/issue/issue_comment.service.ts +++ b/web/services/issue/issue_comment.service.ts @@ -1,6 +1,6 @@ import { APIService } from "services/api.service"; // types -import { IIssueActivity } from "@plane/types"; +import { TIssueComment } from "@plane/types"; // helper import { API_BASE_URL } from "helpers/common.helper"; @@ -9,8 +9,22 @@ export class IssueCommentService extends APIService { super(API_BASE_URL); } - async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`) + async getIssueComments( + workspaceSlug: string, + projectId: string, + issueId: string, + params: + | { + created_at__gt: string; + } + | {} = {} + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, { + params: { + activity_type: "issue-comment", + ...params, + }, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -21,7 +35,7 @@ export class IssueCommentService extends APIService { workspaceSlug: string, projectId: string, issueId: string, - data: Partial + data: Partial ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data) .then((response) => response?.data) @@ -35,7 +49,7 @@ export class IssueCommentService extends APIService { projectId: string, issueId: string, commentId: string, - data: Partial + data: Partial ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`, diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 87cb50092..479fa4bf0 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -271,6 +271,11 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, { issues: issueIds, }); + + issueIds.map((issueId) => { + this.rootIssueStore.issues.updateIssue(issueId, { cycle_id: cycleId }); + }); + return issueToCycle; } catch (error) { throw error; diff --git a/web/store/issue/issue-details/activity.store.ts b/web/store/issue/issue-details/activity.store.ts index 168a3f540..efa181c95 100644 --- a/web/store/issue/issue-details/activity.store.ts +++ b/web/store/issue/issue-details/activity.store.ts @@ -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 sortBy from "lodash/sortBy"; +import update from "lodash/update"; +import concat from "lodash/concat"; +import uniq from "lodash/uniq"; // services -import { IssueService } from "services/issue"; +import { IssueActivityService } from "services/issue"; // types 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 { // actions - fetchActivities: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + fetchActivities: ( + workspaceSlug: string, + projectId: string, + issueId: string, + loaderType?: TActivityLoader + ) => Promise; } export interface IIssueActivityStore extends IIssueActivityStoreActions { // observables + loader: TActivityLoader; activities: TIssueActivityIdMap; activityMap: TIssueActivityMap; - // computed - issueActivities: string[] | undefined; // helper methods getActivitiesByIssueId: (issueId: string) => string[] | undefined; getActivityById: (activityId: string) => TIssueActivity | undefined; + getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined; } export class IssueActivityStore implements IIssueActivityStore { // observables + loader: TActivityLoader = "fetch"; activities: TIssueActivityIdMap = {}; activityMap: TIssueActivityMap = {}; // root store rootIssueDetailStore: IIssueDetail; // services - issueService; + issueActivityService; constructor(rootStore: IIssueDetail) { makeObservable(this, { // observables + loader: observable.ref, activities: observable, activityMap: observable, - // computed - issueActivities: computed, // actions fetchActivities: action, }); // root store this.rootIssueDetailStore = rootStore; // services - this.issueService = new IssueService(); - } - - // computed - get issueActivities() { - const issueId = this.rootIssueDetailStore.peekIssue?.issueId; - if (!issueId) return undefined; - return this.activities[issueId] ?? undefined; + this.issueActivityService = new IssueActivityService(); } // helper methods @@ -65,17 +69,73 @@ export class IssueActivityStore implements IIssueActivityStore { 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 - fetchActivities = async (workspaceSlug: string, projectId: string, issueId: string) => { + fetchActivities = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + loaderType: TActivityLoader = "fetch" + ) => { 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); + runInAction(() => { - set(this.activities, issueId, activityIds); + update(this.activities, issueId, (_activityIds) => { + if (!_activityIds) return activityIds; + return uniq(concat(_activityIds, activityIds)); + }); activities.forEach((activity) => { set(this.activityMap, activity.id, activity); }); + this.loader = undefined; }); return activities; diff --git a/web/store/issue/issue-details/comment.store.ts b/web/store/issue/issue-details/comment.store.ts index 613eb0fad..4336971de 100644 --- a/web/store/issue/issue-details/comment.store.ts +++ b/web/store/issue/issue-details/comment.store.ts @@ -1,31 +1,55 @@ -import { action, makeObservable, runInAction } from "mobx"; +import { action, makeObservable, observable, runInAction } from "mobx"; 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 import { IssueCommentService } from "services/issue"; // types 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 { + fetchComments: ( + workspaceSlug: string, + projectId: string, + issueId: string, + loaderType?: TCommentLoader + ) => Promise; createComment: ( workspaceSlug: string, projectId: string, issueId: string, - data: Partial + data: Partial ) => Promise; updateComment: ( workspaceSlug: string, projectId: string, issueId: string, commentId: string, - data: Partial + data: Partial ) => Promise; removeComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise; } -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 { + // observables + loader: TCommentLoader = "fetch"; + comments: TIssueCommentIdMap = {}; + commentMap: TIssueCommentMap = {}; // root store rootIssueDetail: IIssueDetail; // services @@ -33,7 +57,12 @@ export class IssueCommentStore implements IIssueCommentStore { constructor(rootStore: IIssueDetail) { makeObservable(this, { + // observables + loader: observable.ref, + comments: observable, + commentMap: observable, // actions + fetchComments: action, createComment: action, updateComment: action, removeComment: action, @@ -44,13 +73,64 @@ export class IssueCommentStore implements IIssueCommentStore { this.issueCommentService = new IssueCommentService(); } - createComment = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + // 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) => { try { const response = await this.issueCommentService.createIssueComment(workspaceSlug, projectId, issueId, data); runInAction(() => { - this.rootIssueDetail.activity.activities[issueId].push(response.id); - set(this.rootIssueDetail.activity.activityMap, response.id, response); + update(this.comments, issueId, (_commentIds) => { + if (!_commentIds) return [response.id]; + return uniq(concat(_commentIds, [response.id])); + }); + set(this.commentMap, response.id, response); }); return response; @@ -64,12 +144,12 @@ export class IssueCommentStore implements IIssueCommentStore { projectId: string, issueId: string, commentId: string, - data: Partial + data: Partial ) => { try { runInAction(() => { 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 { const response = await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId); - const reactionIndex = this.rootIssueDetail.activity.activities[issueId].findIndex( - (_comment) => _comment === commentId - ); - if (reactionIndex >= 0) - runInAction(() => { - this.rootIssueDetail.activity.activities[issueId].splice(reactionIndex, 1); - delete this.rootIssueDetail.activity.activityMap[commentId]; - }); + runInAction(() => { + pull(this.comments[issueId], commentId); + delete this.commentMap[commentId]; + }); return response; } catch (error) { diff --git a/web/store/issue/issue-details/comment_reaction.store.ts b/web/store/issue/issue-details/comment_reaction.store.ts index 3cf629456..59adeef62 100644 --- a/web/store/issue/issue-details/comment_reaction.store.ts +++ b/web/store/issue/issue-details/comment_reaction.store.ts @@ -1,10 +1,16 @@ import { action, makeObservable, observable, runInAction } from "mobx"; 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 import { IssueReactionService } from "services/issue"; // types import { IIssueDetail } from "./root.store"; import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; +// helpers +import { groupReactions } from "helpers/emoji.helper"; export interface IIssueCommentReactionStoreActions { // actions @@ -13,6 +19,7 @@ export interface IIssueCommentReactionStoreActions { projectId: string, commentId: string ) => Promise; + applyCommentReactions: (commentId: string, commentReactions: TIssueCommentReaction[]) => void; createCommentReaction: ( workspaceSlug: string, projectId: string, @@ -23,7 +30,8 @@ export interface IIssueCommentReactionStoreActions { workspaceSlug: string, projectId: string, commentId: string, - reaction: string + reaction: string, + userId: string ) => Promise; } @@ -32,8 +40,9 @@ export interface IIssueCommentReactionStore extends IIssueCommentReactionStoreAc commentReactions: TIssueCommentReactionIdMap; commentReactionMap: TIssueCommentReactionMap; // helper methods - getCommentReactionsByCommentId: (commentId: string) => string[] | undefined; + getCommentReactionsByCommentId: (commentId: string) => { [reaction_id: string]: string[] } | undefined; getCommentReactionById: (reactionId: string) => TIssueCommentReaction | undefined; + commentReactionsByUser: (commentId: string, userId: string) => TIssueCommentReaction[]; } export class IssueCommentReactionStore implements IIssueCommentReactionStore { @@ -52,6 +61,7 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore { commentReactionMap: observable, // actions fetchCommentReactions: action, + applyCommentReactions: action, createCommentReaction: action, removeCommentReaction: action, }); @@ -72,25 +82,67 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore { 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 fetchCommentReactions = async (workspaceSlug: string, projectId: string, commentId: string) => { 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); - runInAction(() => { - set(this.commentReactions, commentId, reactionIds); - reactions.forEach((reaction) => { - set(this.commentReactionMap, reaction.id, reaction); - }); + const groupedReactions = groupReactions(response || [], "reaction"); + + const commentReactionIdsMap: { [reaction: string]: string[] } = {}; + + 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) { 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) => { try { const response = await this.issueReactionService.createIssueCommentReaction(workspaceSlug, projectId, commentId, { @@ -98,7 +150,10 @@ export class IssueCommentReactionStore implements IIssueCommentReactionStore { }); 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); }); @@ -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 { - const reactionIndex = this.commentReactions[commentId].findIndex((_reaction) => _reaction === reaction); - if (reactionIndex >= 0) + const userReactions = this.commentReactionsByUser(commentId, userId); + const currentReaction = find(userReactions, { actor: userId, reaction: reaction }); + + if (currentReaction && currentReaction.id) { runInAction(() => { - this.commentReactions[commentId].splice(reactionIndex, 1); + pull(this.commentReactions[commentId][reaction], currentReaction.id); delete this.commentReactionMap[reaction]; }); + } const response = await this.issueReactionService.deleteIssueCommentReaction( workspaceSlug, diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index 0a552b196..ba429b6dd 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -77,6 +77,9 @@ export class IssueStore implements IIssueStore { // fetch issue activity this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + // fetch issue comments + this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId); + // fetch issue subscription 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) => - this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + 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) => this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); - addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => - this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { + 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) => - this.rootIssueDetailStore.rootIssueStore.cycleIssues.removeIssueFromCycle( + removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + const cycle = await this.rootIssueDetailStore.rootIssueStore.cycleIssues.removeIssueFromCycle( workspaceSlug, projectId, cycleId, issueId ); + await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + return cycle; + }; - addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => - this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule( + addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => { + const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.addIssueToModule( workspaceSlug, projectId, moduleId, 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) => - this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssueFromModule( + removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { + const _module = await this.rootIssueDetailStore.rootIssueStore.moduleIssues.removeIssueFromModule( workspaceSlug, projectId, moduleId, issueId ); + await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + return _module; + }; } diff --git a/web/store/issue/issue-details/link.store.ts b/web/store/issue/issue-details/link.store.ts index e5760e802..751cdcaa1 100644 --- a/web/store/issue/issue-details/link.store.ts +++ b/web/store/issue/issue-details/link.store.ts @@ -101,6 +101,8 @@ export class IssueLinkStore implements IIssueLinkStore { set(this.linkMap, response.id, response); }); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { throw error; @@ -123,6 +125,8 @@ export class IssueLinkStore implements IIssueLinkStore { const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { // TODO: fetch issue detail @@ -141,6 +145,8 @@ export class IssueLinkStore implements IIssueLinkStore { delete this.linkMap[linkId]; }); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { throw error; diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index 59c7edd7c..6b15f4445 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -79,10 +79,11 @@ export class IssueReactionStore implements IIssueReactionStore { const _userReactions: TIssueReaction[] = []; Object.keys(reactions).forEach((reaction) => { - reactions[reaction].map((reactionId) => { - const currentReaction = this.getReactionById(reactionId); - if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); - }); + if (reactions?.[reaction]) + reactions?.[reaction].map((reactionId) => { + const currentReaction = this.getReactionById(reactionId); + if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); + }); }); return _userReactions; @@ -126,6 +127,8 @@ export class IssueReactionStore implements IIssueReactionStore { set(this.reactionMap, response.id, response); }); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { throw error; @@ -152,6 +155,8 @@ export class IssueReactionStore implements IIssueReactionStore { const response = await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { throw error; diff --git a/web/store/issue/issue-details/relation.store.ts b/web/store/issue/issue-details/relation.store.ts index 6eb865b6d..da729540e 100644 --- a/web/store/issue/issue-details/relation.store.ts +++ b/web/store/issue/issue-details/relation.store.ts @@ -124,6 +124,8 @@ export class IssueRelationStore implements IIssueRelationStore { }); }); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { throw error; @@ -149,6 +151,8 @@ export class IssueRelationStore implements IIssueRelationStore { related_issue, }); + // fetching activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return response; } catch (error) { this.fetchRelations(workspaceSlug, projectId, issueId); diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index c03c92130..21f66adee 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -3,20 +3,20 @@ import { action, computed, makeObservable, observable } from "mobx"; import { IIssueRootStore } from "../root.store"; import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.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 { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.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 = { workspaceSlug: string; @@ -27,14 +27,14 @@ export type TPeekIssue = { export interface IIssueDetail extends IIssueStoreActions, IIssueReactionStoreActions, - IIssueActivityStoreActions, - IIssueCommentStoreActions, - IIssueCommentReactionStoreActions, IIssueLinkStoreActions, IIssueSubIssuesStoreActions, IIssueSubscriptionStoreActions, IIssueAttachmentStoreActions, - IIssueRelationStoreActions { + IIssueRelationStoreActions, + IIssueActivityStoreActions, + IIssueCommentStoreActions, + IIssueCommentReactionStoreActions { // observables peekIssue: TPeekIssue | undefined; isIssueLinkModalOpen: boolean; @@ -72,13 +72,13 @@ export class IssueDetail implements IIssueDetail { issue: IIssueStore; reaction: IIssueReactionStore; attachment: IIssueAttachmentStore; - activity: IIssueActivityStore; - comment: IIssueCommentStore; - commentReaction: IIssueCommentReactionStore; subIssues: IIssueSubIssuesStore; link: IIssueLinkStore; subscription: IIssueSubscriptionStore; relation: IIssueRelationStore; + activity: IIssueActivityStore; + comment: IIssueCommentStore; + commentReaction: IIssueCommentReactionStore; constructor(rootStore: IIssueRootStore) { makeObservable(this, { @@ -150,31 +150,6 @@ export class IssueDetail implements IIssueDetail { userId: string ) => 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) => - this.comment.createComment(workspaceSlug, projectId, issueId, data); - updateComment = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - data: Partial - ) => 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 fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => this.attachment.fetchAttachments(workspaceSlug, projectId, issueId); @@ -240,4 +215,38 @@ export class IssueDetail implements IIssueDetail { relationType: TIssueRelationTypes, relatedIssue: string ) => 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) => + this.comment.createComment(workspaceSlug, projectId, issueId, data); + updateComment = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + data: Partial + ) => 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); } diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index d9612107a..580373416 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -264,6 +264,10 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { issues: issueIds, }); + issueIds.map((issueId) => { + this.rootIssueStore.issues.updateIssue(issueId, { module_id: moduleId }); + }); + return issueToModule; } catch (error) { throw error; diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index a8b1aabeb..3a94b8611 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -146,7 +146,7 @@ export class ProjectPublishStore implements IProjectPublishStore { runInAction(() => { 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; }); return response; @@ -226,7 +226,7 @@ export class ProjectPublishStore implements IProjectPublishStore { ); runInAction(() => { 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; }); return response;