[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,
IssuePublicSerializer,
IssueDetailSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
)
from .module import (

View File

@ -58,9 +58,12 @@ class DynamicBaseSerializer(BaseSerializer):
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueFlatSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
InboxIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
)
# Expansion mapper
@ -79,12 +82,34 @@ class DynamicBaseSerializer(BaseSerializer):
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"parent": IssueLiteSerializer,
"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
@ -105,7 +130,11 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
InboxIssueLiteSerializer,
IssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
)
# Expansion mapper
@ -124,9 +153,13 @@ class DynamicBaseSerializer(BaseSerializer):
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"parent": IssueLiteSerializer,
"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
if expand in expansion:

View File

@ -444,6 +444,22 @@ class IssueLinkSerializer(BaseSerializer):
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 Meta:
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):
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 Meta:
model = CommentReaction
@ -606,48 +649,39 @@ class IssueSerializer(DynamicBaseSerializer):
read_only_fields = fields
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
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:
model = Issue
fields = "__all__"
read_only_fields = [
"start_date",
"target_date",
"completed_at",
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
fields = [
"id",
"sequence_id",
"project_id",
]
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):

View File

@ -3,7 +3,7 @@ import json
# Django import
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.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
@ -25,13 +25,14 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
ProjectMember,
IssueReaction,
IssueSubscriber,
)
from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
IssueCreateSerializer,
IssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activites_task import issue_activity
@ -385,9 +386,7 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None:
issue.state = state
issue.save()
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand)
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
)
@ -397,11 +396,45 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueDetailSerializer(
issue,
expand=self.expand,
)
issue = (
self.get_queryset()
.filter(pk=issue_id)
.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)
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)
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
)
issue = (
self.get_queryset()
.filter(pk=pk)
.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 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):
issue = Issue.objects.get(
@ -560,39 +595,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue = (
self.get_queryset()
.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)
issue = self.get_queryset().filter(pk=pk).first()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None):
@ -1581,13 +1585,47 @@ class IssueArchiveViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
)
issue = (
self.get_queryset()
.filter(pk=pk)
.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 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):
issue = Issue.objects.get(
@ -2286,17 +2324,52 @@ class IssueDraftViewSet(BaseViewSet):
notification=True,
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)
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
)
issue = (
self.get_queryset()
.filter(pk=pk)
.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 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):
issue = Issue.objects.get(

View File

@ -1,4 +1,7 @@
import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link";
import { TIssueReaction } from "./issue_reaction";
// new issue structure types
export type TIssue = {
@ -34,7 +37,12 @@ export type TIssue = {
updated_by: string;
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?: string;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { FC, useState } from "react";
// hooks
import useToast from "hooks/use-toast";
import { useIssueDetail } from "hooks/store";
import { useIssueDetail, useMember } from "hooks/store";
// ui
import { ExternalLinkIcon, Tooltip } from "@plane/ui";
// icons
@ -26,6 +26,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
toggleIssueLinkModal: toggleIssueLinkModalStore,
link: { getLinkById },
} = useIssueDetail();
const { getUserDetails } = useMember();
const { setToastAlert } = useToast();
// state
@ -38,6 +39,8 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>;
const createdByDetails = getUserDetails(linkDetail.created_by_id);
return (
<div key={linkId}>
<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">
Added {calculateTimeAgo(linkDetail.created_at)}
<br />
by{" "}
{linkDetail.created_by_detail.is_bot
? linkDetail.created_by_detail.first_name + " Bot"
: linkDetail.created_by_detail.display_name}
{createdByDetails && (
<>
by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
</>
)}
</p>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export interface IArchivedIssues {
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions
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>;
quickAddIssue: undefined;
}
@ -111,15 +111,13 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
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);
if (issueIndex >= 0)
runInAction(() => {
this.issues[projectId].splice(issueIndex, 1);
});
return response;
} catch (error) {
throw error;
}

View File

@ -41,13 +41,13 @@ export interface ICycleIssues {
issueId: string,
data: Partial<TIssue>,
cycleId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
removeIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
cycleId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
quickAddIssue: (
workspaceSlug: string,
projectId: string,
@ -207,9 +207,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
try {
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);
return response;
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
throw error;
@ -225,7 +224,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
try {
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);
const issueIndex = this.issues[cycleId].findIndex((_issueId) => _issueId === issueId);
@ -233,8 +232,6 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
runInAction(() => {
this.issues[cycleId].splice(issueIndex, 1);
});
return response;
} catch (error) {
throw error;
}

View File

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

View File

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

View File

@ -2,15 +2,15 @@ import { makeObservable } from "mobx";
// services
import { IssueArchiveService, IssueService } from "services/issue";
// types
import { IIssueDetail } from "./root.store";
import { TIssue } from "@plane/types";
import { computedFn } from "mobx-utils";
import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions {
// actions
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: 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>;
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) => {
try {
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);
if (!issue) throw new Error("Issue not found");
@ -75,13 +76,15 @@ export class IssueStore implements IIssueStore {
// state
// issue reactions
this.rootIssueDetailStore.reaction.fetchReactions(workspaceSlug, projectId, issueId);
if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issueId, issue.issue_reactions);
// fetch issue links
this.rootIssueDetailStore.link.fetchLinks(workspaceSlug, projectId, issueId);
if (issue.issue_link) this.rootIssueDetailStore.addLinks(issueId, issue.issue_link);
// 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
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
@ -89,9 +92,6 @@ export class IssueStore implements IIssueStore {
// fetch issue comments
this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId);
// fetch issue subscription
this.rootIssueDetailStore.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
// fetch sub issues
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>) => {
const issue = await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(
workspaceSlug,
projectId,
issueId,
data
);
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) =>

View File

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

View File

@ -14,6 +14,7 @@ import { groupReactions } from "helpers/emoji.helper";
export interface IIssueReactionStoreActions {
// actions
addReactions: (issueId: string, reactions: TIssueReaction[]) => void;
fetchReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssueReaction[]>;
createReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<any>;
removeReaction: (
@ -50,6 +51,7 @@ export class IssueReactionStore implements IIssueReactionStore {
reactions: observable,
reactionMap: observable,
// actions
addReactions: action.bound,
fetchReactions: action,
createReaction: action,
removeReaction: action,
@ -82,30 +84,35 @@ export class IssueReactionStore implements IIssueReactionStore {
if (reactions?.[reaction])
reactions?.[reaction].map((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;
};
addReactions = (issueId: string, reactions: TIssueReaction[]) => {
const groupedReactions = groupReactions(reactions || [], "reaction");
const issueReactionIdsMap: { [reaction: string]: string[] } = {};
Object.keys(groupedReactions).map((reactionId) => {
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
issueReactionIdsMap[reactionId] = reactionIds;
});
runInAction(() => {
set(this.reactions, issueId, issueReactionIdsMap);
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);
const groupedReactions = groupReactions(response || [], "reaction");
const issueReactionIdsMap: { [reaction: string]: string[] } = {};
Object.keys(groupedReactions).map((reactionId) => {
const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id);
issueReactionIdsMap[reactionId] = reactionIds;
});
runInAction(() => {
set(this.reactions, issueId, issueReactionIdsMap);
response.forEach((reaction) => set(this.reactionMap, reaction.id, reaction));
});
this.addReactions(issueId, response);
return response;
} catch (error) {
@ -144,7 +151,7 @@ export class IssueReactionStore implements IIssueReactionStore {
) => {
try {
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) {
runInAction(() => {

View File

@ -15,8 +15,15 @@ import {
IssueCommentReactionStore,
IIssueCommentReactionStoreActions,
} from "./comment_reaction.store";
import { TIssue, TIssueComment, TIssueCommentReaction, TIssueLink, TIssueRelationTypes } from "@plane/types";
import {
TIssue,
TIssueAttachment,
TIssueComment,
TIssueCommentReaction,
TIssueLink,
TIssueReaction,
TIssueRelationTypes,
} from "@plane/types";
export type TPeekIssue = {
workspaceSlug: string;
@ -151,6 +158,7 @@ export class IssueDetail implements IIssueDetail {
this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
// reactions
addReactions = (issueId: string, reactions: TIssueReaction[]) => this.reaction.addReactions(issueId, reactions);
fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.reaction.fetchReactions(workspaceSlug, projectId, issueId);
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);
// attachments
addAttachments = (issueId: string, attachments: TIssueAttachment[]) =>
this.attachment.addAttachments(issueId, attachments);
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.attachment.fetchAttachments(workspaceSlug, projectId, issueId);
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);
// link
addLinks = (issueId: string, links: TIssueLink[]) => this.link.addLinks(issueId, links);
fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.link.fetchLinks(workspaceSlug, projectId, issueId);
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);
// subscription
addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) =>
this.subscription.addSubscription(issueId, isSubscribed);
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId);
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";
export interface IIssueSubscriptionStoreActions {
fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void;
fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<boolean>;
createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
}
export interface IIssueSubscriptionStore extends IIssueSubscriptionStoreActions {
// 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
getSubscriptionByIssueId: (issueId: string) => Record<string, boolean> | undefined;
getSubscriptionByIssueId: (issueId: string) => boolean | undefined;
}
export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// observables
subscriptionMap: Record<string, Record<string, Record<string, boolean>>> = {};
subscriptionMap: Record<string, Record<string, boolean>> = {};
// root store
rootIssueDetail: IIssueDetail;
// services
@ -31,6 +32,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// observables
subscriptionMap: observable,
// actions
addSubscription: action.bound,
fetchSubscriptions: action,
createSubscription: action,
removeSubscription: action,
@ -49,22 +51,26 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined;
};
addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) => {
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
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 currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
if (!currentUserId) throw new Error("user id not available");
const subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
workspaceSlug,
projectId,
issueId
);
runInAction(() => {
set(this.subscriptionMap, [issueId, currentUserId], subscription);
});
this.addSubscription(issueId, subscription?.subscribed);
return subscription;
return subscription?.subscribed;
} catch (error) {
throw error;
}
@ -79,9 +85,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], { subscribed: true });
});
const response = await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
return response;
await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error;
@ -97,13 +101,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], { subscribed: false });
});
const response = await this.notificationService.unsubscribeFromIssueNotifications(
workspaceSlug,
projectId,
issueId
);
return response;
await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error;

View File

@ -39,13 +39,13 @@ export interface IModuleIssues {
issueId: string,
data: Partial<TIssue>,
moduleId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
removeIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
quickAddIssue: (
workspaceSlug: string,
projectId: string,
@ -212,9 +212,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
try {
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);
return response;
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);
throw error;
@ -230,7 +229,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
try {
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);
const issueIndex = this.issues[moduleId].findIndex((_issueId) => _issueId === issueId);
@ -238,8 +237,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
runInAction(() => {
this.issues[moduleId].splice(issueIndex, 1);
});
return response;
} catch (error) {
throw error;
}

View File

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

View File

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

View File

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

View File

@ -30,13 +30,13 @@ export interface IWorkspaceIssues {
issueId: string,
data: Partial<TIssue>,
viewId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
removeIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
viewId?: string | undefined
) => Promise<TIssue | undefined>;
) => Promise<void>;
}
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");
this.rootStore.issues.updateIssue(issueId, data);
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
return response;
await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
} catch (error) {
if (viewId) this.fetchIssues(workspaceSlug, viewId, "mutation");
throw error;
@ -184,7 +183,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
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);
if (issueIndex >= 0)
@ -193,8 +192,6 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
});
this.rootStore.issues.removeIssue(issueId);
return response;
} catch (error) {
throw error;
}