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):
return (
Issue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
Issue.objects.filter(
project_id=self.kwargs.get("project_id")
)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True)
.select_related("workspace", "project", "state", "parent")
@ -1710,7 +1704,7 @@ class IssueDraftViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
@ -1832,7 +1826,10 @@ class IssueDraftViewSet(BaseViewSet):
notification=True,
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)
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)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
issue = self.get_queryset().filter(pk=pk).first()
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):
issue = Issue.objects.get(

View File

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

View File

@ -103,7 +103,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<Breadcrumbs.BreadcrumbItem
type="text"
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>

View File

@ -66,16 +66,22 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
</div>
</WithDisplayPropertiesHOC>
<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"
>
{issue?.is_draft ? (
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</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
className="flex flex-wrap items-center gap-2 whitespace-nowrap"

View File

@ -79,21 +79,14 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
return (
<>
{isDraftIssue ? (
<CreateUpdateDraftIssueModal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
prePopulateData={issuePayload}
fieldsToShow={["all"]}
/>
) : (
<CreateUpdateIssueModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
/>
)}
<CreateUpdateIssueModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
isDraft={isDraftIssue}
/>
{renderExistingIssueModal && (
<ExistingIssuesListModal
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" />
)}
<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"
>
{issue?.is_draft ? (
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</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">
{!issue?.tempId ? (

View File

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

View File

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

View File

@ -21,6 +21,7 @@ export interface DraftIssueProps {
onClose: (saveDraftIssueInLocalStorage?: boolean) => void;
onSubmit: (formData: Partial<TIssue>) => Promise<void>;
projectId: string;
isDraft: boolean;
}
const issueDraftService = new IssueDraftService();
@ -35,6 +36,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
projectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
isDraft,
} = props;
// states
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
@ -107,6 +109,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
onClose={handleClose}
onSubmit={onSubmit}
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 { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
@ -55,8 +55,9 @@ export interface IssueFormProps {
onCreateMoreToggleChange: (value: boolean) => void;
onChange?: (formData: Partial<TIssue> | null) => void;
onClose: () => void;
onSubmit: (values: Partial<TIssue>) => Promise<void>;
onSubmit: (values: Partial<TIssue>, is_draft_issue?: boolean) => Promise<void>;
projectId: string;
isDraft: boolean;
}
// services
@ -72,6 +73,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
projectId: defaultProjectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
isDraft,
} = props;
// states
const [labelModal, setLabelModal] = useState(false);
@ -137,8 +139,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>) => {
await onSubmit(formData);
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
await onSubmit(formData, is_draft_issue);
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="flex items-center gap-x-2">
{/* 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}>
Discard
</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"}
</Button>
</div>

View File

@ -20,10 +20,19 @@ export interface IssuesModalProps {
onSubmit?: (res: TIssue) => Promise<void>;
withDraftIssueWrapper?: boolean;
storeType?: TCreateModalStoreTypes;
isDraft?: boolean;
}
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
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
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: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT);
// store mapping based on current store
const issueStores = {
[EIssuesStoreType.PROJECT]: {
@ -122,11 +132,16 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
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;
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();
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;
const payload: Partial<TIssue> = {
@ -222,7 +237,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
};
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);
if (response != undefined && onSubmit) await onSubmit(response);
@ -274,6 +289,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
projectId={activeProjectId}
isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange}
isDraft={isDraft}
/>
) : (
<IssueFormRoot
@ -287,6 +303,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
onCreateMoreToggleChange={handleCreateMoreToggleChange}
onSubmit={handleFormSubmit}
projectId={activeProjectId}
isDraft={isDraft}
/>
)}
</Dialog.Panel>

View File

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