[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,17 +2310,10 @@ class IssueDraftViewSet(BaseViewSet):
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 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(
type="issue_draft.activity.updated",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),

View File

@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
id: inboxIssueId,
state: "SUCCESS",
element: "Inbox page",
}
},
});
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<DayPicker
selected={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"
className="border border-custom-border-200 rounded-md p-3"
disabled={[{
before: tomorrow,
}]}
disabled={[
{
before: tomorrow,
},
]}
/>
<Button
variant="primary"

View File

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

View File

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

View File

@ -13,6 +13,8 @@ export type TIssueLabel = {
projectId: string;
issueId: string;
disabled: boolean;
isInboxIssue?: boolean;
onLabelUpdate?: (labelIds: string[]) => void;
};
export type TLabelOperations = {
@ -21,7 +23,7 @@ export type TLabelOperations = {
};
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
const { updateIssue } = useIssueDetail();
const { createLabel } = useLabel();
@ -31,12 +33,14 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
() => ({
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
else await updateIssue(workspaceSlug, projectId, issueId, data);
if (!isInboxIssue)
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
} catch (error) {
setToastAlert({
title: "Issue update failed",
@ -48,11 +52,12 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
try {
const labelResponse = await createLabel(workspaceSlug, projectId, data);
setToastAlert({
title: "Label created successfully",
type: "success",
message: "Label created successfully",
});
if (!isInboxIssue)
setToastAlert({
title: "Label created successfully",
type: "success",
message: "Label created successfully",
});
return labelResponse;
} catch (error) {
setToastAlert({
@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
}
},
}),
[updateIssue, createLabel, setToastAlert]
[updateIssue, createLabel, setToastAlert, onLabelUpdate]
);
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(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
},
["id"]
);
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
return (
<>
<DeleteIssueModal

View File

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

View File

@ -27,7 +27,7 @@ import {
StateDropdown,
} from "components/dropdowns";
// ui
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
useEffect(() => {
if (data?.description_html) setValue("description_html", data?.description_html);
}, [data?.description_html]);
const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
@ -365,80 +369,105 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)}
/>
<div className="relative">
<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 && (
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
>
{iAmFeelingLucky ? (
"Generating response"
) : (
<>
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</>
)}
</button>
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
{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">
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
>
<Sparkle className="h-4 w-4" />
AI
{iAmFeelingLucky ? (
"Generating response"
) : (
<>
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</>
)}
</button>
}
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
/>
)}
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditorWithRef
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
initialValue={data?.description_html}
customClassName="min-h-[7rem] border-custom-border-100"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
handleFormChange();
}}
mentionHighlights={mentionHighlights}
mentionSuggestions={mentionSuggestions}
// tabIndex={2}
/>
)}
/>
)}
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditorWithRef
cancelUploadImage={fileService.cancelUpload}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[7rem] border-custom-border-100"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
handleFormChange();
}}
mentionHighlights={mentionHighlights}
mentionSuggestions={mentionSuggestions}
// tabIndex={2}
/>
)}
/>
</Fragment>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<Controller

View File

@ -3,7 +3,16 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
// 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 useLocalStorage from "hooks/use-local-storage";
// components
@ -39,6 +48,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
const [createMore, setCreateMore] = useState(false);
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
const [description, setDescription] = useState<string | undefined>(undefined);
// store hooks
const { captureIssueEvent } = useEventTracker();
const {
@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
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
const issueStores = {
[EIssuesStoreType.PROJECT]: {
@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
// current store details
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(() => {
// fetching issue details
if (isOpen) fetchIssueDetail(data?.id);
// if modal is closed, reset active project to null
// and return to avoid activeProjectId being set to some other project
if (!isOpen) {
@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
// in the url. This has the least priority.
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
// clearing up the description state when we leave the component
return () => setDescription(undefined);
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
try {
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);
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;
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({
type: "success",
title: "Success!",
@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
changesMade={changesMade}
data={{
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : 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
data={{
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : 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 {
is_archived?: boolean;
is_draft?: boolean;
}
export type TIssuePeekOperations = {
@ -45,7 +46,7 @@ export type TIssuePeekOperations = {
};
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { is_archived = false } = props;
const { is_archived = false, is_draft = false } = props;
// hooks
const { setToastAlert } = useToast();
// router
@ -72,7 +73,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
() => ({
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
await fetchIssue(
workspaceSlug,
projectId,
issueId,
is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT"
);
} catch (error) {
console.error("Error fetching the parent issue");
}
@ -302,6 +308,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}),
[
is_archived,
is_draft,
fetchIssue,
updateIssue,
removeIssue,

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { makeObservable } from "mobx";
// services
import { IssueArchiveService, IssueService } from "services/issue";
import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
// types
import { TIssue } from "@plane/types";
import { computedFn } from "mobx-utils";
@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions {
// 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>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore {
// services
issueService;
issueArchiveService;
issueDraftService;
constructor(rootStore: IIssueDetail) {
makeObservable(this, {});
@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore {
// services
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
this.issueDraftService = new IssueDraftService();
}
// helper methods
@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore {
});
// actions
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => {
try {
const query = {
expand: "issue_reactions,issue_attachment,issue_link,parent",
};
let issue: TIssue;
let issuePayload: TIssue;
if (isArchived)
if (issueType === "ARCHIVED")
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);
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
// parent

View File

@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail {
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
// issue
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) =>
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived);
fetchIssue = async (
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>) =>
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>