[WEB-468] fix: issue detail endpoints (#3722)

* dev: add is_subscriber to issue details endpoint

* dev: remove is_subscribed annotation from detail serializers

* dev: update issue details endpoint

* dev: inbox issue create

* dev: issue detail serializer

* dev: optimize and add extra fields for issue details

* dev: remove data from issue updates

* dev: add fields for issue link and attachment

* remove expecting a issue response while updating and deleting an issue

* change link, attachment and reaction types and modify store to recieve their data from within the issue detail API call

* make changes for subscription store to recieve data from issue detail API call

* dev: add issue reaction id

* add query prarms for archived issue

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
This commit is contained in:
Nikhil 2024-02-22 20:58:34 +05:30 committed by GitHub
parent 7927b7678d
commit 03e5f4a5bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 490 additions and 324 deletions

View File

@ -69,6 +69,9 @@ from .issue import (
RelatedIssueSerializer, RelatedIssueSerializer,
IssuePublicSerializer, IssuePublicSerializer,
IssueDetailSerializer, IssueDetailSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
) )
from .module import ( from .module import (

View File

@ -58,9 +58,12 @@ class DynamicBaseSerializer(BaseSerializer):
IssueSerializer, IssueSerializer,
LabelSerializer, LabelSerializer,
CycleIssueSerializer, CycleIssueSerializer,
IssueFlatSerializer, IssueLiteSerializer,
IssueRelationSerializer, IssueRelationSerializer,
InboxIssueLiteSerializer InboxIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
) )
# Expansion mapper # Expansion mapper
@ -79,12 +82,34 @@ class DynamicBaseSerializer(BaseSerializer):
"assignees": UserLiteSerializer, "assignees": UserLiteSerializer,
"labels": LabelSerializer, "labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer, "issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer, "parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer, "issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer, "issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_attachment": IssueAttachmentLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
} }
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False) self.fields[field] = expansion[field](
many=(
True
if field
in [
"members",
"assignees",
"labels",
"issue_cycle",
"issue_relation",
"issue_inbox",
"issue_reactions",
"issue_attachment",
"issue_link",
"sub_issues",
]
else False
)
)
return self.fields return self.fields
@ -105,7 +130,11 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer, LabelSerializer,
CycleIssueSerializer, CycleIssueSerializer,
IssueRelationSerializer, IssueRelationSerializer,
InboxIssueLiteSerializer InboxIssueLiteSerializer,
IssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
) )
# Expansion mapper # Expansion mapper
@ -124,9 +153,13 @@ class DynamicBaseSerializer(BaseSerializer):
"assignees": UserLiteSerializer, "assignees": UserLiteSerializer,
"labels": LabelSerializer, "labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer, "issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer, "parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer, "issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer, "issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_attachment": IssueAttachmentLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
} }
# Check if field in expansion then expand the field # Check if field in expansion then expand the field
if expand in expansion: if expand in expansion:

View File

@ -444,6 +444,22 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**validated_data) return IssueLink.objects.create(**validated_data)
class IssueLinkLiteSerializer(BaseSerializer):
class Meta:
model = IssueLink
fields = [
"id",
"issue_id",
"title",
"url",
"metadata",
"created_by_id",
"created_at",
]
read_only_fields = fields
class IssueAttachmentSerializer(BaseSerializer): class IssueAttachmentSerializer(BaseSerializer):
class Meta: class Meta:
model = IssueAttachment model = IssueAttachment
@ -459,6 +475,21 @@ class IssueAttachmentSerializer(BaseSerializer):
] ]
class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueAttachment
fields = [
"id",
"asset",
"attributes",
"issue_id",
"updated_at",
"updated_by_id",
]
read_only_fields = fields
class IssueReactionSerializer(BaseSerializer): class IssueReactionSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor") actor_detail = UserLiteSerializer(read_only=True, source="actor")
@ -473,6 +504,18 @@ class IssueReactionSerializer(BaseSerializer):
] ]
class IssueReactionLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueReaction
fields = [
"id",
"actor_id",
"issue_id",
"reaction",
]
class CommentReactionSerializer(BaseSerializer): class CommentReactionSerializer(BaseSerializer):
class Meta: class Meta:
model = CommentReaction model = CommentReaction
@ -606,48 +649,39 @@ class IssueSerializer(DynamicBaseSerializer):
read_only_fields = fields read_only_fields = fields
class IssueDetailSerializer(IssueSerializer): class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField() description_html = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True) is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta): class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"] fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
class IssueLiteSerializer(DynamicBaseSerializer): class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
class Meta: class Meta:
model = Issue model = Issue
fields = "__all__" fields = [
read_only_fields = [ "id",
"start_date", "sequence_id",
"target_date", "project_id",
"completed_at",
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
] ]
read_only_fields = fields
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
is_subscribed = serializers.BooleanField()
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
read_only_fields = fields
class IssuePublicSerializer(BaseSerializer): class IssuePublicSerializer(BaseSerializer):

View File

@ -3,7 +3,7 @@ import json
# Django import # Django import
from django.utils import timezone from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Exists
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
@ -25,13 +25,14 @@ from plane.db.models import (
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
ProjectMember, ProjectMember,
IssueReaction,
IssueSubscriber,
) )
from plane.app.serializers import ( from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer, IssueSerializer,
InboxSerializer, InboxSerializer,
InboxIssueSerializer, InboxIssueSerializer,
IssueCreateSerializer,
IssueDetailSerializer,
) )
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
@ -385,9 +386,7 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
issue = self.get_queryset().filter(pk=issue_id).first() return Response(status=status.HTTP_204_NO_CONTENT)
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response( return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST serializer.errors, status=status.HTTP_400_BAD_REQUEST
) )
@ -397,11 +396,45 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, inbox_id, issue_id): def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = self.get_queryset().filter(pk=issue_id).first() issue = (
serializer = IssueDetailSerializer( self.get_queryset()
issue, .filter(pk=issue_id)
expand=self.expand, .prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
) )
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
).first()
if issue is None:
return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = IssueSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id): def destroy(self, request, slug, project_id, inbox_id, issue_id):

View File

@ -528,13 +528,48 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first() issue = (
return Response( self.get_queryset()
IssueDetailSerializer( .filter(pk=pk)
issue, fields=self.fields, expand=self.expand .prefetch_related(
).data, Prefetch(
status=status.HTTP_200_OK, "issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
) )
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
).first()
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk=None): def partial_update(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(
@ -560,39 +595,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
issue = ( issue = self.get_queryset().filter(pk=pk).first()
self.get_queryset() return Response(status=status.HTTP_204_NO_CONTENT)
.filter(pk=pk)
.values(
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_draft",
"archived_at",
)
.first()
)
return Response(issue, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None):
@ -1581,13 +1585,47 @@ class IssueArchiveViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first() issue = (
return Response( self.get_queryset()
IssueDetailSerializer( .filter(pk=pk)
issue, fields=self.fields, expand=self.expand .prefetch_related(
).data, Prefetch(
status=status.HTTP_200_OK, "issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
) )
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
).first()
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def unarchive(self, request, slug, project_id, pk=None): def unarchive(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(
@ -2286,17 +2324,52 @@ class IssueDraftViewSet(BaseViewSet):
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first() issue = (
return Response( self.get_queryset()
IssueSerializer( .filter(pk=pk)
issue, fields=self.fields, expand=self.expand .prefetch_related(
).data, Prefetch(
status=status.HTTP_200_OK, "issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
) )
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
)
).first()
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(

View File

@ -1,4 +1,7 @@
import { TIssuePriorities } from "../issues"; import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link";
import { TIssueReaction } from "./issue_reaction";
// new issue structure types // new issue structure types
export type TIssue = { export type TIssue = {
@ -34,7 +37,12 @@ export type TIssue = {
updated_by: string; updated_by: string;
is_draft: boolean; is_draft: boolean;
is_subscribed: boolean; is_subscribed?: boolean;
parent?: partial<TIssue>;
issue_reactions?: TIssueReaction[];
issue_attachment?: TIssueAttachment[];
issue_link?: TIssueLink[];
// tempId is used for optimistic updates. It is not a part of the API response. // tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string; tempId?: string;

View File

@ -1,17 +1,15 @@
export type TIssueAttachment = { export type TIssueAttachment = {
id: string; id: string;
created_at: string;
updated_at: string;
attributes: { attributes: {
name: string; name: string;
size: number; size: number;
}; };
asset: string; asset: string;
created_by: string; issue_id: string;
//need
updated_at: string;
updated_by: string; updated_by: string;
project: string;
workspace: string;
issue: string;
}; };
export type TIssueAttachmentMap = { export type TIssueAttachmentMap = {

View File

@ -4,11 +4,13 @@ export type TIssueLinkEditableFields = {
}; };
export type TIssueLink = TIssueLinkEditableFields & { export type TIssueLink = TIssueLinkEditableFields & {
created_at: Date; created_by_id: string;
created_by: string;
created_by_detail: IUserLite;
id: string; id: string;
metadata: any; metadata: any;
issue_id: string;
//need
created_at: Date;
}; };
export type TIssueLinkMap = { export type TIssueLinkMap = {

View File

@ -1,15 +1,8 @@
export type TIssueReaction = { export type TIssueReaction = {
actor: string; actor_id: string;
actor_detail: IUserLite;
created_at: Date;
created_by: string;
id: string; id: string;
issue: string; issue_id: string;
project: string;
reaction: string; reaction: string;
updated_at: Date;
updated_by: string;
workspace: string;
}; };
export type TIssueReactionMap = { export type TIssueReactionMap = {

View File

@ -196,9 +196,9 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const updateDraftIssue = async (payload: Partial<TIssue>) => { const updateDraftIssue = async (payload: Partial<TIssue>) => {
await draftIssues await draftIssues
.updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
.then((res) => { .then(() => {
if (isUpdatingSingleIssue) { if (isUpdatingSingleIssue) {
mutate<TIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); mutate<TIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...payload } as TIssue), false);
} else { } else {
if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString())); if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString()));
} }

View File

@ -1,7 +1,7 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useIssueDetail } from "hooks/store"; import { useIssueDetail, useMember } from "hooks/store";
// ui // ui
import { ExternalLinkIcon, Tooltip } from "@plane/ui"; import { ExternalLinkIcon, Tooltip } from "@plane/ui";
// icons // icons
@ -26,6 +26,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
toggleIssueLinkModal: toggleIssueLinkModalStore, toggleIssueLinkModal: toggleIssueLinkModalStore,
link: { getLinkById }, link: { getLinkById },
} = useIssueDetail(); } = useIssueDetail();
const { getUserDetails } = useMember();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// state // state
@ -38,6 +39,8 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
const linkDetail = getLinkById(linkId); const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>; if (!linkDetail) return <></>;
const createdByDetails = getUserDetails(linkDetail.created_by_id);
return ( return (
<div key={linkId}> <div key={linkId}>
<IssueLinkCreateUpdateModal <IssueLinkCreateUpdateModal
@ -110,10 +113,11 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300"> <p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(linkDetail.created_at)} Added {calculateTimeAgo(linkDetail.created_at)}
<br /> <br />
by{" "} {createdByDetails && (
{linkDetail.created_by_detail.is_bot <>
? linkDetail.created_by_detail.first_name + " Bot" by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
: linkDetail.created_by_detail.display_name} </>
)}
</p> </p>
</div> </div>
</div> </div>

View File

@ -96,7 +96,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
showToast: boolean = true showToast: boolean = true
) => { ) => {
try { try {
const response = await updateIssue(workspaceSlug, projectId, issueId, data); await updateIssue(workspaceSlug, projectId, issueId, data);
if (showToast) { if (showToast) {
setToastAlert({ setToastAlert({
title: "Issue updated successfully", title: "Issue updated successfully",
@ -106,7 +106,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
} }
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","), change_details: Object.values(data).join(","),

View File

@ -1,11 +1,12 @@
import { FC, useState } from "react";
import { Bell, BellOff } from "lucide-react"; import { Bell, BellOff } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { FC, useState } from "react";
// UI // UI
import { Button, Loader } from "@plane/ui"; import { Button, Loader } from "@plane/ui";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import isNil from "lodash/isNil";
export type TIssueSubscription = { export type TIssueSubscription = {
workspaceSlug: string; workspaceSlug: string;
@ -25,17 +26,17 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
// state // state
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const subscription = getSubscriptionByIssueId(issueId); const isSubscribed = getSubscriptionByIssueId(issueId);
const handleSubscription = async () => { const handleSubscription = async () => {
setLoading(true); setLoading(true);
try { try {
if (subscription?.subscribed) await removeSubscription(workspaceSlug, projectId, issueId); if (isSubscribed) await removeSubscription(workspaceSlug, projectId, issueId);
else await createSubscription(workspaceSlug, projectId, issueId); else await createSubscription(workspaceSlug, projectId, issueId);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`,
message: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`,
}); });
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
@ -48,42 +49,32 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
} }
}; };
if (!subscription) if (isNil(isSubscribed))
return ( return (
<Loader> <Loader>
<Loader.Item width="92px" height="27px" /> <Loader.Item width="106px" height="28px" />
</Loader> </Loader>
); );
return ( return (
<>
{subscription ? (
<div> <div>
<Button <Button
size="sm" size="sm"
prependIcon={subscription?.subscribed ? <BellOff /> : <Bell className="h-3 w-3" />} prependIcon={isSubscribed ? <BellOff /> : <Bell className="h-3 w-3" />}
variant="outline-primary" variant="outline-primary"
className="hover:!bg-custom-primary-100/20" className="hover:!bg-custom-primary-100/20"
onClick={handleSubscription} onClick={handleSubscription}
> >
{loading ? ( {loading ? (
<span> <span>
<span className="hidden sm:block">Loading...</span> <span className="hidden sm:block">Loading</span>...
</span> </span>
) : subscription?.subscribed ? ( ) : isSubscribed ? (
<div className="hidden sm:block">Unsubscribe</div> <div className="hidden sm:block">Unsubscribe</div>
) : ( ) : (
<div className="hidden sm:block">Subscribe</div> <div className="hidden sm:block">Subscribe</div>
)} )}
</Button> </Button>
</div> </div>
) : (
<>
<Loader>
<Loader.Item height="28px" width="106px" />
</Loader>
</>
)}
</>
); );
}); });

View File

@ -183,7 +183,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (!workspaceSlug || !payload.project_id || !data?.id) return; if (!workspaceSlug || !payload.project_id || !data?.id) return;
try { try {
const response = await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -191,11 +191,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}); });
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS" }, payload: { ...payload, issueId: data.id, state: "SUCCESS" },
path: router.asPath, path: router.asPath,
}); });
handleClose(); handleClose();
return response;
} catch (error) { } catch (error) {
setToastAlert({ setToastAlert({
type: "error", type: "error",

View File

@ -15,7 +15,6 @@ import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
interface IIssuePeekOverview { interface IIssuePeekOverview {
is_archived?: boolean; is_archived?: boolean;
onIssueUpdate?: (issue: Partial<TIssue>) => Promise<void>;
} }
export type TIssuePeekOperations = { export type TIssuePeekOperations = {
@ -46,7 +45,7 @@ export type TIssuePeekOperations = {
}; };
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => { export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { is_archived = false, onIssueUpdate } = props; const { is_archived = false } = props;
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// router // router
@ -87,7 +86,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
) => { ) => {
try { try {
const response = await updateIssue(workspaceSlug, projectId, issueId, data); const response = await updateIssue(workspaceSlug, projectId, issueId, data);
if (onIssueUpdate) await onIssueUpdate(response);
if (showToast) if (showToast)
setToastAlert({ setToastAlert({
title: "Issue updated successfully", title: "Issue updated successfully",
@ -96,7 +94,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}); });
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_UPDATED, eventName: ISSUE_UPDATED,
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","), change_details: Object.values(data).join(","),
@ -314,7 +312,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
removeIssueFromModule, removeIssueFromModule,
removeModulesFromIssue, removeModulesFromIssue,
setToastAlert, setToastAlert,
onIssueUpdate,
captureIssueEvent, captureIssueEvent,
router.asPath, router.asPath,
] ]

View File

@ -1,14 +1,7 @@
// services // services
import { APIService } from "services/api.service"; import { APIService } from "services/api.service";
// type // type
import type { import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types";
TIssue,
IIssueDisplayProperties,
ILinkDetails,
TIssueLink,
TIssueSubIssues,
TIssueActivity,
} from "@plane/types";
// helper // helper
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
@ -211,7 +204,7 @@ export class IssueService extends APIService {
projectId: string, projectId: string,
issueId: string, issueId: string,
data: Partial<TIssueLink> data: Partial<TIssueLink>
): Promise<ILinkDetails> { ): Promise<TIssueLink> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -225,7 +218,7 @@ export class IssueService extends APIService {
issueId: string, issueId: string,
linkId: string, linkId: string,
data: Partial<TIssueLink> data: Partial<TIssueLink>
): Promise<ILinkDetails> { ): Promise<TIssueLink> {
return this.patch( return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`,
data data

View File

@ -1,6 +1,7 @@
import { APIService } from "services/api.service"; import { APIService } from "services/api.service";
// type // type
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import { TIssue } from "@plane/types";
export class IssueArchiveService extends APIService { export class IssueArchiveService extends APIService {
constructor() { constructor() {
@ -25,8 +26,15 @@ export class IssueArchiveService extends APIService {
}); });
} }
async retrieveArchivedIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> { async retrieveArchivedIssue(
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`) workspaceSlug: string,
projectId: string,
issueId: string,
queries?: any
): Promise<TIssue> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`, {
params: queries,
})
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;

View File

@ -17,7 +17,7 @@ export interface IArchivedIssues {
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions // actions
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue>; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined; quickAddIssue: undefined;
} }
@ -111,15 +111,13 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId); const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId);
if (issueIndex >= 0) if (issueIndex >= 0)
runInAction(() => { runInAction(() => {
this.issues[projectId].splice(issueIndex, 1); this.issues[projectId].splice(issueIndex, 1);
}); });
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -41,13 +41,13 @@ export interface ICycleIssues {
issueId: string, issueId: string,
data: Partial<TIssue>, data: Partial<TIssue>,
cycleId?: string | undefined cycleId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
removeIssue: ( removeIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
cycleId?: string | undefined cycleId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
quickAddIssue: ( quickAddIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -207,9 +207,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
try { try {
if (!cycleId) throw new Error("Cycle Id is required"); if (!cycleId) throw new Error("Cycle Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
throw error; throw error;
@ -225,7 +224,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
try { try {
if (!cycleId) throw new Error("Cycle Id is required"); if (!cycleId) throw new Error("Cycle Id is required");
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
const issueIndex = this.issues[cycleId].findIndex((_issueId) => _issueId === issueId); const issueIndex = this.issues[cycleId].findIndex((_issueId) => _issueId === issueId);
@ -233,8 +232,6 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
runInAction(() => { runInAction(() => {
this.issues[cycleId].splice(issueIndex, 1); this.issues[cycleId].splice(issueIndex, 1);
}); });
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -22,8 +22,8 @@ export interface IDraftIssues {
// actions // actions
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined; quickAddIssue: undefined;
} }
@ -141,7 +141,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try { try {
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
runInAction(() => { runInAction(() => {
@ -151,8 +151,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
}); });
}); });
} }
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error; throw error;
@ -161,7 +159,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
runInAction(() => { runInAction(() => {
update(this.issues, [projectId], (issueIds = []) => { update(this.issues, [projectId], (issueIds = []) => {
@ -169,8 +167,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
return issueIds; return issueIds;
}); });
}); });
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -11,6 +11,7 @@ import { IIssueDetail } from "./root.store";
import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types";
export interface IIssueAttachmentStoreActions { export interface IIssueAttachmentStoreActions {
addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void;
fetchAttachments: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueAttachment[]>; fetchAttachments: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueAttachment[]>;
createAttachment: ( createAttachment: (
workspaceSlug: string, workspaceSlug: string,
@ -54,6 +55,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
// computed // computed
issueAttachments: computed, issueAttachments: computed,
// actions // actions
addAttachments: action.bound,
fetchAttachments: action, fetchAttachments: action,
createAttachment: action, createAttachment: action,
removeAttachment: action, removeAttachment: action,
@ -83,17 +85,21 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
}; };
// actions // actions
addAttachments = (issueId: string, attachments: TIssueAttachment[]) => {
if (attachments && attachments.length > 0) {
const _attachmentIds = attachments.map((attachment) => attachment.id);
runInAction(() => {
update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, _attachmentIds)));
attachments.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment));
});
}
};
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => { fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.issueAttachmentService.getIssueAttachment(workspaceSlug, projectId, issueId); const response = await this.issueAttachmentService.getIssueAttachment(workspaceSlug, projectId, issueId);
if (response && response.length > 0) { this.addAttachments(issueId, response);
const _attachmentIds = response.map((attachment) => attachment.id);
runInAction(() => {
update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, _attachmentIds)));
response.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment));
});
}
return response; return response;
} catch (error) { } catch (error) {

View File

@ -2,15 +2,15 @@ import { makeObservable } from "mobx";
// services // services
import { IssueArchiveService, IssueService } from "services/issue"; import { IssueArchiveService, IssueService } from "services/issue";
// types // types
import { IIssueDetail } from "./root.store";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions { export interface IIssueStoreActions {
// actions // actions
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>; fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
@ -54,12 +54,13 @@ export class IssueStore implements IIssueStore {
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
try { try {
const query = { const query = {
expand: "state,assignees,labels,parent", expand: "issue_reactions,issue_attachment,issue_link,parent",
}; };
let issue: any; let issue: TIssue;
if (isArchived) issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId); if (isArchived)
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query); else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
if (!issue) throw new Error("Issue not found"); if (!issue) throw new Error("Issue not found");
@ -75,13 +76,15 @@ export class IssueStore implements IIssueStore {
// state // state
// issue reactions // issue reactions
this.rootIssueDetailStore.reaction.fetchReactions(workspaceSlug, projectId, issueId); if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issueId, issue.issue_reactions);
// fetch issue links // fetch issue links
this.rootIssueDetailStore.link.fetchLinks(workspaceSlug, projectId, issueId); if (issue.issue_link) this.rootIssueDetailStore.addLinks(issueId, issue.issue_link);
// fetch issue attachments // fetch issue attachments
this.rootIssueDetailStore.attachment.fetchAttachments(workspaceSlug, projectId, issueId); if (issue.issue_attachment) this.rootIssueDetailStore.addAttachments(issueId, issue.issue_attachment);
this.rootIssueDetailStore.addSubscription(issueId, issue.is_subscribed);
// fetch issue activity // fetch issue activity
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
@ -89,9 +92,6 @@ export class IssueStore implements IIssueStore {
// fetch issue comments // fetch issue comments
this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId); this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId);
// fetch issue subscription
this.rootIssueDetailStore.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
// fetch sub issues // fetch sub issues
this.rootIssueDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId); this.rootIssueDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId);
@ -109,14 +109,8 @@ export class IssueStore implements IIssueStore {
}; };
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
const issue = await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue( await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
workspaceSlug,
projectId,
issueId,
data
);
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
return issue;
}; };
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>

View File

@ -7,16 +7,22 @@ import { IIssueDetail } from "./root.store";
import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types";
export interface IIssueLinkStoreActions { export interface IIssueLinkStoreActions {
addLinks: (issueId: string, links: TIssueLink[]) => void;
fetchLinks: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueLink[]>; fetchLinks: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueLink[]>;
createLink: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueLink>) => Promise<any>; createLink: (
workspaceSlug: string,
projectId: string,
issueId: string,
data: Partial<TIssueLink>
) => Promise<TIssueLink>;
updateLink: ( updateLink: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
linkId: string, linkId: string,
data: Partial<TIssueLink> data: Partial<TIssueLink>
) => Promise<any>; ) => Promise<TIssueLink>;
removeLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise<any>; removeLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise<void>;
} }
export interface IIssueLinkStore extends IIssueLinkStoreActions { export interface IIssueLinkStore extends IIssueLinkStoreActions {
@ -47,6 +53,7 @@ export class IssueLinkStore implements IIssueLinkStore {
// computed // computed
issueLinks: computed, issueLinks: computed,
// actions // actions
addLinks: action.bound,
fetchLinks: action, fetchLinks: action,
createLink: action, createLink: action,
updateLink: action, updateLink: action,
@ -77,15 +84,17 @@ export class IssueLinkStore implements IIssueLinkStore {
}; };
// actions // actions
addLinks = (issueId: string, links: TIssueLink[]) => {
runInAction(() => {
this.links[issueId] = links.map((link) => link.id);
links.forEach((link) => set(this.linkMap, link.id, link));
});
};
fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => { fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.issueService.fetchIssueLinks(workspaceSlug, projectId, issueId); const response = await this.issueService.fetchIssueLinks(workspaceSlug, projectId, issueId);
this.addLinks(issueId, response);
runInAction(() => {
this.links[issueId] = response.map((link) => link.id);
response.forEach((link) => set(this.linkMap, link.id, link));
});
return response; return response;
} catch (error) { } catch (error) {
throw error; throw error;
@ -136,7 +145,7 @@ export class IssueLinkStore implements IIssueLinkStore {
removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => { removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => {
try { try {
const response = await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId); await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
const linkIndex = this.links[issueId].findIndex((_comment) => _comment === linkId); const linkIndex = this.links[issueId].findIndex((_comment) => _comment === linkId);
if (linkIndex >= 0) if (linkIndex >= 0)
@ -147,7 +156,6 @@ export class IssueLinkStore implements IIssueLinkStore {
// fetching activity // fetching activity
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -14,6 +14,7 @@ import { groupReactions } from "helpers/emoji.helper";
export interface IIssueReactionStoreActions { export interface IIssueReactionStoreActions {
// actions // actions
addReactions: (issueId: string, reactions: TIssueReaction[]) => void;
fetchReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueReaction[]>; fetchReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueReaction[]>;
createReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<any>; createReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<any>;
removeReaction: ( removeReaction: (
@ -50,6 +51,7 @@ export class IssueReactionStore implements IIssueReactionStore {
reactions: observable, reactions: observable,
reactionMap: observable, reactionMap: observable,
// actions // actions
addReactions: action.bound,
fetchReactions: action, fetchReactions: action,
createReaction: action, createReaction: action,
removeReaction: action, removeReaction: action,
@ -82,18 +84,15 @@ export class IssueReactionStore implements IIssueReactionStore {
if (reactions?.[reaction]) if (reactions?.[reaction])
reactions?.[reaction].map((reactionId) => { reactions?.[reaction].map((reactionId) => {
const currentReaction = this.getReactionById(reactionId); const currentReaction = this.getReactionById(reactionId);
if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); if (currentReaction && currentReaction.actor_id === userId) _userReactions.push(currentReaction);
}); });
}); });
return _userReactions; return _userReactions;
}; };
// actions addReactions = (issueId: string, reactions: TIssueReaction[]) => {
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => { const groupedReactions = groupReactions(reactions || [], "reaction");
try {
const response = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId);
const groupedReactions = groupReactions(response || [], "reaction");
const issueReactionIdsMap: { [reaction: string]: string[] } = {}; const issueReactionIdsMap: { [reaction: string]: string[] } = {};
@ -104,8 +103,16 @@ export class IssueReactionStore implements IIssueReactionStore {
runInAction(() => { runInAction(() => {
set(this.reactions, issueId, issueReactionIdsMap); set(this.reactions, issueId, issueReactionIdsMap);
response.forEach((reaction) => set(this.reactionMap, reaction.id, reaction)); reactions.forEach((reaction) => set(this.reactionMap, reaction.id, reaction));
}); });
};
// actions
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const response = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId);
this.addReactions(issueId, response);
return response; return response;
} catch (error) { } catch (error) {
@ -144,7 +151,7 @@ export class IssueReactionStore implements IIssueReactionStore {
) => { ) => {
try { try {
const userReactions = this.reactionsByUser(issueId, userId); const userReactions = this.reactionsByUser(issueId, userId);
const currentReaction = find(userReactions, { actor: userId, reaction: reaction }); const currentReaction = find(userReactions, { actor_id: userId, reaction: reaction });
if (currentReaction && currentReaction.id) { if (currentReaction && currentReaction.id) {
runInAction(() => { runInAction(() => {

View File

@ -15,8 +15,15 @@ import {
IssueCommentReactionStore, IssueCommentReactionStore,
IIssueCommentReactionStoreActions, IIssueCommentReactionStoreActions,
} from "./comment_reaction.store"; } from "./comment_reaction.store";
import {
import { TIssue, TIssueComment, TIssueCommentReaction, TIssueLink, TIssueRelationTypes } from "@plane/types"; TIssue,
TIssueAttachment,
TIssueComment,
TIssueCommentReaction,
TIssueLink,
TIssueReaction,
TIssueRelationTypes,
} from "@plane/types";
export type TPeekIssue = { export type TPeekIssue = {
workspaceSlug: string; workspaceSlug: string;
@ -151,6 +158,7 @@ export class IssueDetail implements IIssueDetail {
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
// reactions // reactions
addReactions = (issueId: string, reactions: TIssueReaction[]) => this.reaction.addReactions(issueId, reactions);
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.reaction.fetchReactions(workspaceSlug, projectId, issueId); this.reaction.fetchReactions(workspaceSlug, projectId, issueId);
createReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => createReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) =>
@ -164,6 +172,8 @@ export class IssueDetail implements IIssueDetail {
) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId); ) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId);
// attachments // attachments
addAttachments = (issueId: string, attachments: TIssueAttachment[]) =>
this.attachment.addAttachments(issueId, attachments);
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.attachment.fetchAttachments(workspaceSlug, projectId, issueId); this.attachment.fetchAttachments(workspaceSlug, projectId, issueId);
createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, data: FormData) => createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, data: FormData) =>
@ -172,6 +182,7 @@ export class IssueDetail implements IIssueDetail {
this.attachment.removeAttachment(workspaceSlug, projectId, issueId, attachmentId); this.attachment.removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
// link // link
addLinks = (issueId: string, links: TIssueLink[]) => this.link.addLinks(issueId, links);
fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.link.fetchLinks(workspaceSlug, projectId, issueId); this.link.fetchLinks(workspaceSlug, projectId, issueId);
createLink = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueLink>) => createLink = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssueLink>) =>
@ -206,6 +217,8 @@ export class IssueDetail implements IIssueDetail {
this.subIssues.deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); this.subIssues.deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
// subscription // subscription
addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) =>
this.subscription.addSubscription(issueId, isSubscribed);
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId); this.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
createSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => createSubscription = async (workspaceSlug: string, projectId: string, issueId: string) =>

View File

@ -6,21 +6,22 @@ import { NotificationService } from "services/notification.service";
import { IIssueDetail } from "./root.store"; import { IIssueDetail } from "./root.store";
export interface IIssueSubscriptionStoreActions { export interface IIssueSubscriptionStoreActions {
fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>; addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void;
createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>; fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<boolean>;
removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>; createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
} }
export interface IIssueSubscriptionStore extends IIssueSubscriptionStoreActions { export interface IIssueSubscriptionStore extends IIssueSubscriptionStoreActions {
// observables // observables
subscriptionMap: Record<string, Record<string, Record<string, boolean>>>; // Record defines subscriptionId as key and link as value subscriptionMap: Record<string, Record<string, boolean>>; // Record defines subscriptionId as key and link as value
// helper methods // helper methods
getSubscriptionByIssueId: (issueId: string) => Record<string, boolean> | undefined; getSubscriptionByIssueId: (issueId: string) => boolean | undefined;
} }
export class IssueSubscriptionStore implements IIssueSubscriptionStore { export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// observables // observables
subscriptionMap: Record<string, Record<string, Record<string, boolean>>> = {}; subscriptionMap: Record<string, Record<string, boolean>> = {};
// root store // root store
rootIssueDetail: IIssueDetail; rootIssueDetail: IIssueDetail;
// services // services
@ -31,6 +32,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// observables // observables
subscriptionMap: observable, subscriptionMap: observable,
// actions // actions
addSubscription: action.bound,
fetchSubscriptions: action, fetchSubscriptions: action,
createSubscription: action, createSubscription: action,
removeSubscription: action, removeSubscription: action,
@ -49,22 +51,26 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined; return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined;
}; };
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => { addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) => {
try {
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId; const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
if (!currentUserId) throw new Error("user id not available"); if (!currentUserId) throw new Error("user id not available");
runInAction(() => {
set(this.subscriptionMap, [issueId, currentUserId], isSubscribed ?? false);
});
};
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const subscription = await this.notificationService.getIssueNotificationSubscriptionStatus( const subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
workspaceSlug, workspaceSlug,
projectId, projectId,
issueId issueId
); );
runInAction(() => { this.addSubscription(issueId, subscription?.subscribed);
set(this.subscriptionMap, [issueId, currentUserId], subscription);
});
return subscription; return subscription?.subscribed;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -79,9 +85,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], { subscribed: true }); set(this.subscriptionMap, [issueId, currentUserId], { subscribed: true });
}); });
const response = await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId); await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
return response;
} catch (error) { } catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId); this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error; throw error;
@ -97,13 +101,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], { subscribed: false }); set(this.subscriptionMap, [issueId, currentUserId], { subscribed: false });
}); });
const response = await this.notificationService.unsubscribeFromIssueNotifications( await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
workspaceSlug,
projectId,
issueId
);
return response;
} catch (error) { } catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId); this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error; throw error;

View File

@ -39,13 +39,13 @@ export interface IModuleIssues {
issueId: string, issueId: string,
data: Partial<TIssue>, data: Partial<TIssue>,
moduleId?: string | undefined moduleId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
removeIssue: ( removeIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
moduleId?: string | undefined moduleId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
quickAddIssue: ( quickAddIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -212,9 +212,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
try { try {
if (!moduleId) throw new Error("Module Id is required"); if (!moduleId) throw new Error("Module Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
throw error; throw error;
@ -230,7 +229,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
try { try {
if (!moduleId) throw new Error("Module Id is required"); if (!moduleId) throw new Error("Module Id is required");
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
const issueIndex = this.issues[moduleId].findIndex((_issueId) => _issueId === issueId); const issueIndex = this.issues[moduleId].findIndex((_issueId) => _issueId === issueId);
@ -238,8 +237,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
runInAction(() => { runInAction(() => {
this.issues[moduleId].splice(issueIndex, 1); this.issues[moduleId].splice(issueIndex, 1);
}); });
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -41,13 +41,13 @@ export interface IProfileIssues {
issueId: string, issueId: string,
data: Partial<TIssue>, data: Partial<TIssue>,
userId?: string | undefined userId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
removeIssue: ( removeIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
userId?: string | undefined userId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
quickAddIssue: undefined; quickAddIssue: undefined;
} }
@ -221,14 +221,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
if (!userId) throw new Error("user id is required"); if (!userId) throw new Error("user id is required");
this.rootStore.issues.updateIssue(issueId, data); this.rootStore.issues.updateIssue(issueId, data);
const response = await this.rootIssueStore.projectIssues.updateIssue( await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, data.id as keyof TIssue, data);
workspaceSlug,
projectId,
data.id as keyof TIssue,
data
);
return response;
} catch (error) { } catch (error) {
if (this.currentView) this.fetchIssues(workspaceSlug, undefined, "mutation", userId, this.currentView); if (this.currentView) this.fetchIssues(workspaceSlug, undefined, "mutation", userId, this.currentView);
throw error; throw error;
@ -243,7 +236,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
) => { ) => {
if (!userId) return; if (!userId) return;
try { try {
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const uniqueViewId = `${workspaceSlug}_${this.currentView}`; const uniqueViewId = `${workspaceSlug}_${this.currentView}`;
@ -252,8 +245,6 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
runInAction(() => { runInAction(() => {
this.issues[userId][uniqueViewId].splice(issueIndex, 1); this.issues[userId][uniqueViewId].splice(issueIndex, 1);
}); });
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -34,13 +34,13 @@ export interface IProjectViewIssues {
issueId: string, issueId: string,
data: Partial<TIssue>, data: Partial<TIssue>,
viewId?: string | undefined viewId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
removeIssue: ( removeIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
viewId?: string | undefined viewId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
quickAddIssue: ( quickAddIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -181,8 +181,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
try { try {
if (!viewId) throw new Error("View Id is required"); if (!viewId) throw new Error("View Id is required");
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error; throw error;
@ -198,15 +197,13 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
try { try {
if (!viewId) throw new Error("View Id is required"); if (!viewId) throw new Error("View Id is required");
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[viewId].findIndex((_issueId) => _issueId === issueId); const issueIndex = this.issues[viewId].findIndex((_issueId) => _issueId === issueId);
if (issueIndex >= 0) if (issueIndex >= 0)
runInAction(() => { runInAction(() => {
this.issues[viewId].splice(issueIndex, 1); this.issues[viewId].splice(issueIndex, 1);
}); });
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error; throw error;

View File

@ -21,8 +21,8 @@ export interface IProjectIssues {
// action // action
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>; quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>;
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>; removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
} }
@ -144,8 +144,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
try { try {
this.rootStore.issues.updateIssue(issueId, data); this.rootStore.issues.updateIssue(issueId, data);
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error; throw error;
@ -154,14 +153,13 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
runInAction(() => { runInAction(() => {
pull(this.issues[projectId], issueId); pull(this.issues[projectId], issueId);
}); });
this.rootStore.issues.removeIssue(issueId); this.rootStore.issues.removeIssue(issueId);
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -30,13 +30,13 @@ export interface IWorkspaceIssues {
issueId: string, issueId: string,
data: Partial<TIssue>, data: Partial<TIssue>,
viewId?: string | undefined viewId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
removeIssue: ( removeIssue: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
viewId?: string | undefined viewId?: string | undefined
) => Promise<TIssue | undefined>; ) => Promise<void>;
} }
export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues { export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues {
@ -165,8 +165,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
if (!viewId) throw new Error("View id is required"); if (!viewId) throw new Error("View id is required");
this.rootStore.issues.updateIssue(issueId, data); this.rootStore.issues.updateIssue(issueId, data);
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
return response;
} catch (error) { } catch (error) {
if (viewId) this.fetchIssues(workspaceSlug, viewId, "mutation"); if (viewId) this.fetchIssues(workspaceSlug, viewId, "mutation");
throw error; throw error;
@ -184,7 +183,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
const uniqueViewId = `${workspaceSlug}_${viewId}`; const uniqueViewId = `${workspaceSlug}_${viewId}`;
const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[uniqueViewId].findIndex((_issueId) => _issueId === issueId); const issueIndex = this.issues[uniqueViewId].findIndex((_issueId) => _issueId === issueId);
if (issueIndex >= 0) if (issueIndex >= 0)
@ -193,8 +192,6 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
}); });
this.rootStore.issues.removeIssue(issueId); this.rootStore.issues.removeIssue(issueId);
return response;
} catch (error) { } catch (error) {
throw error; throw error;
} }