mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
0a35fcfbc0
commit
729b6ac79e
@ -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(
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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()}
|
||||
|
@ -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 ? (
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user