diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 0b5c612d3..34bce8a0a 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -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( diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index e6349e0b4..396003589 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -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 && ( diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 0fe6a74c5..139ec0257 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -103,7 +103,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => { } /> + } /> } /> diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 99d774bd5..203ac4938 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -66,16 +66,22 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > + {issue?.is_draft ? ( {issue.name} - + ) : ( + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + + + )} = observer((props) => { return ( <> - {isDraftIssue ? ( - setIsOpen(false)} - prePopulateData={issuePayload} - fieldsToShow={["all"]} - /> - ) : ( - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - /> - )} + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> + {renderExistingIssueModal && ( = observer((props: IssueBlock
)} - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > + {issue?.is_draft ? ( {issue.name} - + ) : ( + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + + + )}
{!issue?.tempId ? ( diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 49c9f7e40..90270e1a1 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -109,21 +109,13 @@ export const HeaderGroupByCard = observer(
))} - {isDraftIssue ? ( - setIsOpen(false)} - prePopulateData={issuePayload} - fieldsToShow={["all"]} - /> - ) : ( - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - /> - )} + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> {renderExistingIssueModal && ( = (props) => }; delete duplicateIssuePayload.id; + const isDraftIssue = router?.asPath?.includes("draft-issues") || false; + return ( <> = (props) => handleClose={() => setDeleteIssueModal(false)} onSubmit={handleDelete} /> + { @@ -73,7 +76,9 @@ export const ProjectIssueQuickActions: React.FC = (props) => if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} storeType={EIssuesStoreType.PROJECT} + isDraft={isDraftIssue} /> + void; onSubmit: (formData: Partial) => Promise; projectId: string; + isDraft: boolean; } const issueDraftService = new IssueDraftService(); @@ -35,6 +36,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { projectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange, + isDraft, } = props; // states const [issueDiscardModal, setIssueDiscardModal] = useState(false); @@ -107,6 +109,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose={handleClose} onSubmit={onSubmit} projectId={projectId} + isDraft={isDraft} /> ); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 31cb9dd66..430aa4920 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -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 | null) => void; onClose: () => void; - onSubmit: (values: Partial) => Promise; + onSubmit: (values: Partial, is_draft_issue?: boolean) => Promise; projectId: string; + isDraft: boolean; } // services @@ -72,6 +73,7 @@ export const IssueFormRoot: FC = observer((props) => { projectId: defaultProjectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange, + isDraft, } = props; // states const [labelModal, setLabelModal] = useState(false); @@ -137,8 +139,8 @@ export const IssueFormRoot: FC = observer((props) => { const issueName = watch("name"); - const handleFormSubmit = async (formData: Partial) => { - await onSubmit(formData); + const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { + await onSubmit(formData, is_draft_issue); setGptAssistantModal(false); @@ -248,7 +250,7 @@ export const IssueFormRoot: FC = observer((props) => { }} /> )} -
+
{/* Don't show project selection if editing an issue */} @@ -670,7 +672,40 @@ export const IssueFormRoot: FC = observer((props) => { - + ) : ( + + )} + + )} + +
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 3b5b35cea..02a087314 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -20,10 +20,19 @@ export interface IssuesModalProps { onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; storeType?: TCreateModalStoreTypes; + isDraft?: boolean; } export const CreateUpdateIssueModal: React.FC = 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 | null>(null); const [createMore, setCreateMore] = useState(false); @@ -42,6 +51,7 @@ export const CreateUpdateIssueModal: React.FC = 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 = observer((prop onClose(); }; - const handleCreateIssue = async (payload: Partial): Promise => { + const handleCreateIssue = async ( + payload: Partial, + is_draft_issue: boolean = false + ): Promise => { 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 = observer((prop } }; - const handleFormSubmit = async (formData: Partial) => { + const handleFormSubmit = async (formData: Partial, is_draft_issue: boolean = false) => { if (!workspaceSlug || !formData.project_id || !storeType) return; const payload: Partial = { @@ -222,7 +237,7 @@ export const CreateUpdateIssueModal: React.FC = 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 = observer((prop projectId={activeProjectId} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} + isDraft={isDraft} /> ) : ( = observer((prop onCreateMoreToggleChange={handleCreateMoreToggleChange} onSubmit={handleFormSubmit} projectId={activeProjectId} + isDraft={isDraft} /> )} diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index 13d14484a..dc0f601eb 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -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) => { 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) {