[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

@ -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;
}