[WEB-581] fix: issue editing functionality enhancement in Create/Edit modal (#3809)

* chore: draft issue update request

* chore: changed the serializer

* chore: handled issue description in issue modal, inbox issues mutation and draft issue mutaion and changed the endpoints

* chore: handled draft toggle in make a issue payload in issues

* chore: handled issue labels in the inbox issues

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
guru_sainath 2024-02-27 16:58:46 +05:30 committed by GitHub
parent c858b76054
commit 34d6b135f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 250 additions and 156 deletions

View File

@ -2310,16 +2310,9 @@ class IssueDraftViewSet(BaseViewSet):
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )
serializer = IssueSerializer(issue, data=request.data, partial=True) serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if request.data.get(
"is_draft"
) is not None and not request.data.get("is_draft"):
serializer.save(
created_at=timezone.now(), updated_at=timezone.now()
)
else:
serializer.save() serializer.save()
issue_activity.delay( issue_activity.delay(
type="issue_draft.activity.updated", type="issue_draft.activity.updated",

View File

@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
id: inboxIssueId, id: inboxIssueId,
state: "SUCCESS", state: "SUCCESS",
element: "Inbox page", element: "Inbox page",
} },
}); });
router.push({ router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<DayPicker <DayPicker
selected={date ? new Date(date) : undefined} selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined} defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => { if (!date) return; setDate(date) }} onSelect={(date) => {
if (!date) return;
setDate(date);
}}
mode="single" mode="single"
className="border border-custom-border-200 rounded-md p-3" className="border border-custom-border-200 rounded-md p-3"
disabled={[{ disabled={[
{
before: tomorrow, before: tomorrow,
}]} },
]}
/> />
<Button <Button
variant="primary" variant="primary"

View File

@ -54,7 +54,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
showToast: boolean = true showToast: boolean = true
) => { ) => {
try { try {
const response = await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
if (showToast) { if (showToast) {
setToastAlert({ setToastAlert({
title: "Issue updated successfully", title: "Issue updated successfully",
@ -64,7 +64,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
} }
captureIssueEvent({ captureIssueEvent({
eventName: "Inbox issue updated", eventName: "Inbox issue updated",
payload: { ...response, state: "SUCCESS", element: "Inbox" }, payload: { ...data, state: "SUCCESS", element: "Inbox" },
updates: { updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","), change_details: Object.values(data).join(","),

View File

@ -154,6 +154,10 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={!is_editable} disabled={!is_editable}
isInboxIssue
onLabelUpdate={(val: string[]) =>
issueOperations.update(workspaceSlug, projectId, issueId, { label_ids: val })
}
/> />
</div> </div>
</div> </div>

View File

@ -13,6 +13,8 @@ export type TIssueLabel = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
isInboxIssue?: boolean;
onLabelUpdate?: (labelIds: string[]) => void;
}; };
export type TLabelOperations = { export type TLabelOperations = {
@ -21,7 +23,7 @@ export type TLabelOperations = {
}; };
export const IssueLabel: FC<TIssueLabel> = observer((props) => { export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props;
// hooks // hooks
const { updateIssue } = useIssueDetail(); const { updateIssue } = useIssueDetail();
const { createLabel } = useLabel(); const { createLabel } = useLabel();
@ -31,7 +33,9 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
() => ({ () => ({
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try { try {
await updateIssue(workspaceSlug, projectId, issueId, data); if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
else await updateIssue(workspaceSlug, projectId, issueId, data);
if (!isInboxIssue)
setToastAlert({ setToastAlert({
title: "Issue updated successfully", title: "Issue updated successfully",
type: "success", type: "success",
@ -48,6 +52,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => { createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
try { try {
const labelResponse = await createLabel(workspaceSlug, projectId, data); const labelResponse = await createLabel(workspaceSlug, projectId, data);
if (!isInboxIssue)
setToastAlert({ setToastAlert({
title: "Label created successfully", title: "Label created successfully",
type: "success", type: "success",
@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
} }
}, },
}), }),
[updateIssue, createLabel, setToastAlert] [updateIssue, createLabel, setToastAlert, onLabelUpdate]
); );
return ( return (

View File

@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
); );
}; };
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
const duplicateIssuePayload = omit( const duplicateIssuePayload = omit(
{ {
...issue, ...issue,
name: `${issue.name} (copy)`, name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
}, },
["id"] ["id"]
); );
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
return ( return (
<> <>
<DeleteIssueModal <DeleteIssueModal

View File

@ -60,7 +60,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
<DraftKanBanLayout /> <DraftKanBanLayout />
) : null} ) : null}
{/* issue peek overview */} {/* issue peek overview */}
<IssuePeekOverview /> <IssuePeekOverview is_draft />
</div> </div>
)} )}
</div> </div>

View File

@ -27,7 +27,7 @@ import {
StateDropdown, StateDropdown,
} from "components/dropdowns"; } from "components/dropdowns";
// ui // ui
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
// helpers // helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types // types
@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); }, [projectId]);
useEffect(() => {
if (data?.description_html) setValue("description_html", data?.description_html);
}, [data?.description_html]);
const issueName = watch("name"); const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => { const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
@ -365,6 +369,28 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)} )}
/> />
<div className="relative"> <div className="relative">
{data?.description_html === undefined ? (
<Loader className="min-h-[7rem] space-y-2 py-2 border border-custom-border-200 rounded-md p-2 overflow-hidden">
<Loader.Item width="100%" height="26px" />
<div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" />
</div>
<div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" />
</div>
<Loader.Item width="80%" height="26px" />
<div className="flex items-center gap-2">
<Loader.Item width="50%" height="26px" />
</div>
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
<Loader.Item width="100px" height="26px" />
<Loader.Item width="50px" height="26px" />
</div>
</Loader>
) : (
<Fragment>
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2"> <div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
<button <button
@ -428,6 +454,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
? watch("description_html") ? watch("description_html")
: value : value
} }
initialValue={data?.description_html}
customClassName="min-h-[7rem] border-custom-border-100" customClassName="min-h-[7rem] border-custom-border-100"
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
onChange(description_html); onChange(description_html);
@ -439,6 +466,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
/> />
)} )}
/> />
</Fragment>
)}
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Controller <Controller

View File

@ -3,7 +3,16 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// hooks // hooks
import { useApplication, useEventTracker, useCycle, useIssues, useModule, useProject, useWorkspace } from "hooks/store"; import {
useApplication,
useEventTracker,
useCycle,
useIssues,
useModule,
useProject,
useWorkspace,
useIssueDetail,
} from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
@ -39,6 +48,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null); const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
const [activeProjectId, setActiveProjectId] = useState<string | null>(null); const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
const [description, setDescription] = useState<string | undefined>(undefined);
// store hooks // store hooks
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { const {
@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT); const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
const { fetchIssue } = useIssueDetail();
// store mapping based on current store // store mapping based on current store
const issueStores = { const issueStores = {
[EIssuesStoreType.PROJECT]: { [EIssuesStoreType.PROJECT]: {
@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
// current store details // current store details
const { store: currentIssueStore, viewId } = issueStores[storeType]; const { store: currentIssueStore, viewId } = issueStores[storeType];
const fetchIssueDetail = async (issueId: string | undefined) => {
if (!workspaceSlug || !projectId) return;
if (issueId === undefined) {
setDescription("<p></p>");
return;
}
const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT");
if (response) setDescription(response?.description_html || "<p></p>");
};
useEffect(() => { useEffect(() => {
// fetching issue details
if (isOpen) fetchIssueDetail(data?.id);
// if modal is closed, reset active project to null // if modal is closed, reset active project to null
// and return to avoid activeProjectId being set to some other project // and return to avoid activeProjectId being set to some other project
if (!isOpen) { if (!isOpen) {
@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
// in the url. This has the least priority. // in the url. This has the least priority.
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId) if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]); setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
// clearing up the description state when we leave the component
return () => setDescription(undefined);
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]); }, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
const addIssueToCycle = async (issue: TIssue, cycleId: string) => { const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
try { try {
const response = is_draft_issue const response = is_draft_issue
? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload) ? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload)
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); : await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
if (!response) throw new Error(); if (!response) throw new Error();
@ -183,7 +210,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (!workspaceSlug || !payload.project_id || !data?.id) return; if (!workspaceSlug || !payload.project_id || !data?.id) return;
try { try {
await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); isDraft
? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload)
: await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
changesMade={changesMade} changesMade={changesMade}
data={{ data={{
...data, ...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null, cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null, module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
}} }}
@ -276,6 +307,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
<IssueFormRoot <IssueFormRoot
data={{ data={{
...data, ...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null, cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null, module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
}} }}

View File

@ -15,6 +15,7 @@ import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
interface IIssuePeekOverview { interface IIssuePeekOverview {
is_archived?: boolean; is_archived?: boolean;
is_draft?: boolean;
} }
export type TIssuePeekOperations = { export type TIssuePeekOperations = {
@ -45,7 +46,7 @@ export type TIssuePeekOperations = {
}; };
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => { export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { is_archived = false } = props; const { is_archived = false, is_draft = false } = props;
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// router // router
@ -72,7 +73,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
() => ({ () => ({
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
await fetchIssue(workspaceSlug, projectId, issueId, is_archived); await fetchIssue(
workspaceSlug,
projectId,
issueId,
is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT"
);
} catch (error) { } catch (error) {
console.error("Error fetching the parent issue"); console.error("Error fetching the parent issue");
} }
@ -302,6 +308,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}), }),
[ [
is_archived, is_archived,
is_draft,
fetchIssue, fetchIssue,
updateIssue, updateIssue,
removeIssue, removeIssue,

View File

@ -42,7 +42,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}` ? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
: null, : null,
workspaceSlug && projectId && archivedIssueId workspaceSlug && projectId && archivedIssueId
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true) ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), "ARCHIVED")
: null : null
); );

View File

@ -42,8 +42,10 @@ export class IssueDraftService extends APIService {
}); });
} }
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> { async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`) return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, {
params: queries,
})
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response; throw error?.response;

View File

@ -53,7 +53,7 @@ export interface IInboxIssue {
inboxId: string, inboxId: string,
inboxIssueId: string, inboxIssueId: string,
data: Partial<TInboxIssueExtendedDetail> data: Partial<TInboxIssueExtendedDetail>
) => Promise<TInboxIssueExtendedDetail>; ) => Promise<void>;
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>; removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
updateInboxIssueStatus: ( updateInboxIssueStatus: (
workspaceSlug: string, workspaceSlug: string,
@ -61,7 +61,7 @@ export interface IInboxIssue {
inboxId: string, inboxId: string,
inboxIssueId: string, inboxIssueId: string,
data: TInboxDetailedStatus data: TInboxDetailedStatus
) => Promise<TInboxIssueExtendedDetail>; ) => Promise<void>;
} }
export class InboxIssue implements IInboxIssue { export class InboxIssue implements IInboxIssue {
@ -215,22 +215,9 @@ export class InboxIssue implements IInboxIssue {
issue: data, issue: data,
}); });
runInAction(() => { this.rootStore.inbox.rootStore.issue.issues.updateIssue(inboxIssueId, data);
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue);
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
});
runInAction(() => {
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
return uniq(concat(inboxIssueIds, response.id));
});
});
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
return response as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -238,7 +225,7 @@ export class InboxIssue implements IInboxIssue {
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => { removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
try { try {
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
runInAction(() => { runInAction(() => {
pull(this.inboxIssues[inboxId], inboxIssueId); pull(this.inboxIssues[inboxId], inboxIssueId);
@ -248,7 +235,6 @@ export class InboxIssue implements IInboxIssue {
}); });
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
return response as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -262,34 +248,18 @@ export class InboxIssue implements IInboxIssue {
data: TInboxDetailedStatus data: TInboxDetailedStatus
) => { ) => {
try { try {
const response = await this.inboxIssueService.updateInboxIssueStatus( await this.inboxIssueService.updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
workspaceSlug,
projectId,
inboxId,
inboxIssueId,
data
);
const pendingStatus = -2; const pendingStatus = -2;
runInAction(() => { runInAction(() => {
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; set(this.inboxIssueMap, [inboxId, inboxIssueId, "status"], data.status);
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) => update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
data.status === pendingStatus ? count + 1 : count - 1 data.status === pendingStatus ? count + 1 : count - 1
); );
}); });
runInAction(() => {
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
return uniq(concat(inboxIssueIds, response.id));
});
});
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
return response as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -141,7 +141,9 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try { try {
await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
this.rootStore.issues.updateIssue(issueId, data);
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
runInAction(() => { runInAction(() => {

View File

@ -1,6 +1,6 @@
import { makeObservable } from "mobx"; import { makeObservable } from "mobx";
// services // services
import { IssueArchiveService, IssueService } from "services/issue"; import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions { export interface IIssueStoreActions {
// actions // actions
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>; fetchIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
issueType?: "DEFAULT" | "DRAFT" | "ARCHIVED"
) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore {
// services // services
issueService; issueService;
issueArchiveService; issueArchiveService;
issueDraftService;
constructor(rootStore: IIssueDetail) { constructor(rootStore: IIssueDetail) {
makeObservable(this, {}); makeObservable(this, {});
@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore {
// services // services
this.issueService = new IssueService(); this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService(); this.issueArchiveService = new IssueArchiveService();
this.issueDraftService = new IssueDraftService();
} }
// helper methods // helper methods
@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore {
}); });
// actions // actions
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => {
try { try {
const query = { const query = {
expand: "issue_reactions,issue_attachment,issue_link,parent", expand: "issue_reactions,issue_attachment,issue_link,parent",
}; };
let issue: TIssue; let issue: TIssue;
let issuePayload: TIssue;
if (isArchived) if (issueType === "ARCHIVED")
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query); issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
else if (issueType === "DRAFT")
issue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId, query);
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query); else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
if (!issue) throw new Error("Issue not found"); if (!issue) throw new Error("Issue not found");
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true); issuePayload = {
id: issue?.id,
sequence_id: issue?.sequence_id,
name: issue?.name,
description_html: issue?.description_html,
sort_order: issue?.sort_order,
state_id: issue?.state_id,
priority: issue?.priority,
label_ids: issue?.label_ids,
assignee_ids: issue?.assignee_ids,
estimate_point: issue?.estimate_point,
sub_issues_count: issue?.sub_issues_count,
attachment_count: issue?.attachment_count,
link_count: issue?.link_count,
project_id: issue?.project_id,
parent_id: issue?.parent_id,
cycle_id: issue?.cycle_id,
module_ids: issue?.module_ids,
created_at: issue?.created_at,
updated_at: issue?.updated_at,
start_date: issue?.start_date,
target_date: issue?.target_date,
completed_at: issue?.completed_at,
archived_at: issue?.archived_at,
created_by: issue?.created_by,
updated_by: issue?.updated_by,
is_draft: issue?.is_draft,
is_subscribed: issue?.is_subscribed,
};
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload], true);
// store handlers from issue detail // store handlers from issue detail
// parent // parent

View File

@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail {
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
// issue // issue
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => fetchIssue = async (
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived); workspaceSlug: string,
projectId: string,
issueId: string,
issueType: "DEFAULT" | "ARCHIVED" | "DRAFT" = "DEFAULT"
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueType);
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
this.issue.updateIssue(workspaceSlug, projectId, issueId, data); this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>