fix: Handled the draft issue from issue create modal and optimised the draft issue store (#3588)

Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-02-07 20:45:05 +05:30 committed by GitHub
parent 0a35fcfbc0
commit 729b6ac79e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 151 additions and 79 deletions

View File

@ -1668,15 +1668,9 @@ class IssueDraftViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
return ( return (
Issue.objects.annotate( Issue.objects.filter(
sub_issues_count=Issue.issue_objects.filter( project_id=self.kwargs.get("project_id")
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
) )
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True) .filter(is_draft=True)
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
@ -1710,7 +1704,7 @@ class IssueDraftViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
) ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
@ -1832,7 +1826,10 @@ class IssueDraftViewSet(BaseViewSet):
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first()
)
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
@ -1868,10 +1865,13 @@ class IssueDraftViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = self.get_queryset().filter(pk=pk).first()
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True return Response(
IssueSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
) )
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(

View File

@ -163,6 +163,8 @@ export const CommandPalette: FC = observer(() => {
return () => document.removeEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]); }, [handleKeyDown]);
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
if (!currentUser) return null; if (!currentUser) return null;
return ( return (
@ -217,6 +219,7 @@ export const CommandPalette: FC = observer(() => {
onClose={() => toggleCreateIssueModal(false)} onClose={() => toggleCreateIssueModal(false)}
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined} data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
storeType={createIssueStoreType} storeType={createIssueStoreType}
isDraft={isDraftIssue}
/> />
{workspaceSlug && projectId && issueId && issueDetails && ( {workspaceSlug && projectId && issueId && issueDetails && (

View File

@ -103,7 +103,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={ link={
<BreadcrumbLink label="Inbox Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} /> <BreadcrumbLink label="Draft Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
} }
/> />
</Breadcrumbs> </Breadcrumbs>

View File

@ -66,16 +66,22 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
<ControlLink {issue?.is_draft ? (
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span> <span>{issue.name}</span>
</Tooltip> </Tooltip>
</ControlLink> ) : (
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
)}
<IssueProperties <IssueProperties
className="flex flex-wrap items-center gap-2 whitespace-nowrap" className="flex flex-wrap items-center gap-2 whitespace-nowrap"

View File

@ -79,21 +79,14 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
return ( return (
<> <>
{isDraftIssue ? ( <CreateUpdateIssueModal
<CreateUpdateDraftIssueModal isOpen={isOpen}
isOpen={isOpen} onClose={() => setIsOpen(false)}
handleClose={() => setIsOpen(false)} data={issuePayload}
prePopulateData={issuePayload} storeType={storeType}
fieldsToShow={["all"]} isDraft={isDraftIssue}
/> />
) : (
<CreateUpdateIssueModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
/>
)}
{renderExistingIssueModal && ( {renderExistingIssueModal && (
<ExistingIssuesListModal <ExistingIssuesListModal
workspaceSlug={workspaceSlug?.toString()} workspaceSlug={workspaceSlug?.toString()}

View File

@ -69,16 +69,22 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)} )}
<ControlLink {issue?.is_draft ? (
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span> <span>{issue.name}</span>
</Tooltip> </Tooltip>
</ControlLink> ) : (
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
)}
<div className="ml-auto flex flex-shrink-0 items-center gap-2"> <div className="ml-auto flex flex-shrink-0 items-center gap-2">
{!issue?.tempId ? ( {!issue?.tempId ? (

View File

@ -109,21 +109,13 @@ export const HeaderGroupByCard = observer(
</div> </div>
))} ))}
{isDraftIssue ? ( <CreateUpdateIssueModal
<CreateUpdateDraftIssueModal isOpen={isOpen}
isOpen={isOpen} onClose={() => setIsOpen(false)}
handleClose={() => setIsOpen(false)} data={issuePayload}
prePopulateData={issuePayload} storeType={storeType}
fieldsToShow={["all"]} isDraft={isDraftIssue}
/> />
) : (
<CreateUpdateIssueModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
/>
)}
{renderExistingIssueModal && ( {renderExistingIssueModal && (
<ExistingIssuesListModal <ExistingIssuesListModal

View File

@ -54,6 +54,8 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
}; };
delete duplicateIssuePayload.id; delete duplicateIssuePayload.id;
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
return ( return (
<> <>
<DeleteIssueModal <DeleteIssueModal
@ -62,6 +64,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
handleClose={() => setDeleteIssueModal(false)} handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete} onSubmit={handleDelete}
/> />
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={createUpdateIssueModal} isOpen={createUpdateIssueModal}
onClose={() => { onClose={() => {
@ -73,7 +76,9 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
}} }}
storeType={EIssuesStoreType.PROJECT} storeType={EIssuesStoreType.PROJECT}
isDraft={isDraftIssue}
/> />
<CustomMenu <CustomMenu
placement="bottom-start" placement="bottom-start"
customButton={customActionButton} customButton={customActionButton}

View File

@ -21,6 +21,7 @@ export interface DraftIssueProps {
onClose: (saveDraftIssueInLocalStorage?: boolean) => void; onClose: (saveDraftIssueInLocalStorage?: boolean) => void;
onSubmit: (formData: Partial<TIssue>) => Promise<void>; onSubmit: (formData: Partial<TIssue>) => Promise<void>;
projectId: string; projectId: string;
isDraft: boolean;
} }
const issueDraftService = new IssueDraftService(); const issueDraftService = new IssueDraftService();
@ -35,6 +36,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
projectId, projectId,
isCreateMoreToggleEnabled, isCreateMoreToggleEnabled,
onCreateMoreToggleChange, onCreateMoreToggleChange,
isDraft,
} = props; } = props;
// states // states
const [issueDiscardModal, setIssueDiscardModal] = useState(false); const [issueDiscardModal, setIssueDiscardModal] = useState(false);
@ -107,6 +109,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
onClose={handleClose} onClose={handleClose}
onSubmit={onSubmit} onSubmit={onSubmit}
projectId={projectId} projectId={projectId}
isDraft={isDraft}
/> />
</> </>
); );

View File

@ -1,4 +1,4 @@
import React, { FC, useState, useRef, useEffect } from "react"; import React, { FC, useState, useRef, useEffect, Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -55,8 +55,9 @@ export interface IssueFormProps {
onCreateMoreToggleChange: (value: boolean) => void; onCreateMoreToggleChange: (value: boolean) => void;
onChange?: (formData: Partial<TIssue> | null) => void; onChange?: (formData: Partial<TIssue> | null) => void;
onClose: () => void; onClose: () => void;
onSubmit: (values: Partial<TIssue>) => Promise<void>; onSubmit: (values: Partial<TIssue>, is_draft_issue?: boolean) => Promise<void>;
projectId: string; projectId: string;
isDraft: boolean;
} }
// services // services
@ -72,6 +73,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
projectId: defaultProjectId, projectId: defaultProjectId,
isCreateMoreToggleEnabled, isCreateMoreToggleEnabled,
onCreateMoreToggleChange, onCreateMoreToggleChange,
isDraft,
} = props; } = props;
// states // states
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
@ -137,8 +139,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const issueName = watch("name"); const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>) => { const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
await onSubmit(formData); await onSubmit(formData, is_draft_issue);
setGptAssistantModal(false); setGptAssistantModal(false);
@ -248,7 +250,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
}} }}
/> />
)} )}
<form onSubmit={handleSubmit(handleFormSubmit)}> <form>
<div className="space-y-5"> <div className="space-y-5">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
{/* Don't show project selection if editing an issue */} {/* Don't show project selection if editing an issue */}
@ -670,7 +672,40 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={17}> <Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={17}>
Discard Discard
</Button> </Button>
<Button type="submit" variant="primary" size="sm" loading={isSubmitting} tabIndex={18}>
{isDraft && (
<Fragment>
{data?.id ? (
<Button
variant="neutral-primary"
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))}
tabIndex={18}
>
{isSubmitting ? "Moving" : "Move from draft"}
</Button>
) : (
<Button
variant="neutral-primary"
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit(data, true))}
tabIndex={18}
>
{isSubmitting ? "Saving" : "Save as draft"}
</Button>
)}
</Fragment>
)}
<Button
variant="primary"
size="sm"
loading={isSubmitting}
tabIndex={isDraft ? 19 : 18}
onClick={handleSubmit((data) => handleFormSubmit(data))}
>
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"} {data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
</Button> </Button>
</div> </div>

View File

@ -20,10 +20,19 @@ export interface IssuesModalProps {
onSubmit?: (res: TIssue) => Promise<void>; onSubmit?: (res: TIssue) => Promise<void>;
withDraftIssueWrapper?: boolean; withDraftIssueWrapper?: boolean;
storeType?: TCreateModalStoreTypes; storeType?: TCreateModalStoreTypes;
isDraft?: boolean;
} }
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => { export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props; const {
data,
isOpen,
onClose,
onSubmit,
withDraftIssueWrapper = true,
storeType = EIssuesStoreType.PROJECT,
isDraft = false,
} = props;
// states // states
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);
@ -42,6 +51,7 @@ 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);
// store mapping based on current store // store mapping based on current store
const issueStores = { const issueStores = {
[EIssuesStoreType.PROJECT]: { [EIssuesStoreType.PROJECT]: {
@ -122,11 +132,16 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
onClose(); onClose();
}; };
const handleCreateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => { const handleCreateIssue = async (
payload: Partial<TIssue>,
is_draft_issue: boolean = false
): Promise<TIssue | undefined> => {
if (!workspaceSlug || !payload.project_id) return; if (!workspaceSlug || !payload.project_id) return;
try { try {
const response = await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); const response = is_draft_issue
? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload)
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
if (!response) throw new Error(); if (!response) throw new Error();
currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId); currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId);
@ -213,7 +228,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
} }
}; };
const handleFormSubmit = async (formData: Partial<TIssue>) => { const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue: boolean = false) => {
if (!workspaceSlug || !formData.project_id || !storeType) return; if (!workspaceSlug || !formData.project_id || !storeType) return;
const payload: Partial<TIssue> = { const payload: Partial<TIssue> = {
@ -222,7 +237,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
let response: TIssue | undefined = undefined; let response: TIssue | undefined = undefined;
if (!data?.id) response = await handleCreateIssue(payload); if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue);
else response = await handleUpdateIssue(payload); else response = await handleUpdateIssue(payload);
if (response != undefined && onSubmit) await onSubmit(response); if (response != undefined && onSubmit) await onSubmit(response);
@ -274,6 +289,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
projectId={activeProjectId} projectId={activeProjectId}
isCreateMoreToggleEnabled={createMore} isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange} onCreateMoreToggleChange={handleCreateMoreToggleChange}
isDraft={isDraft}
/> />
) : ( ) : (
<IssueFormRoot <IssueFormRoot
@ -287,6 +303,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
onCreateMoreToggleChange={handleCreateMoreToggleChange} onCreateMoreToggleChange={handleCreateMoreToggleChange}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
projectId={activeProjectId} projectId={activeProjectId}
isDraft={isDraft}
/> />
)} )}
</Dialog.Panel> </Dialog.Panel>

View File

@ -1,5 +1,9 @@
import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { action, observable, makeObservable, computed, runInAction } from "mobx";
import set from "lodash/set"; import set from "lodash/set";
import update from "lodash/update";
import uniq from "lodash/uniq";
import concat from "lodash/concat";
import pull from "lodash/pull";
// base class // base class
import { IssueHelperStore } from "../helpers/issue-helper.store"; import { IssueHelperStore } from "../helpers/issue-helper.store";
// services // services
@ -123,7 +127,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data); const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data);
runInAction(() => { runInAction(() => {
this.issues[projectId].push(response.id); update(this.issues, [projectId], (issueIds = []) => uniq(concat(issueIds, response.id)));
}); });
this.rootStore.issues.addIssue([response]); this.rootStore.issues.addIssue([response]);
@ -136,8 +140,17 @@ 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 {
this.rootStore.issues.updateIssue(issueId, data); const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
const response = await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
runInAction(() => {
update(this.issues, [projectId], (issueIds = []) => {
if (issueIds.includes(issueId)) pull(issueIds, issueId);
return issueIds;
});
});
}
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
@ -147,15 +160,14 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try { try {
const response = await this.issueDraftService.deleteDraftIssue(workspaceSlug, projectId, issueId); const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId); runInAction(() => {
if (issueIndex >= 0) update(this.issues, [projectId], (issueIds = []) => {
runInAction(() => { if (issueIds.includes(issueId)) pull(issueIds, issueId);
this.issues[projectId].splice(issueIndex, 1); return issueIds;
}); });
});
this.rootStore.issues.removeIssue(issueId);
return response; return response;
} catch (error) { } catch (error) {