forked from github/plane
feat: draft issues (#2188)
* feat: draft issue issues can be saved as draft * style: modal position
This commit is contained in:
parent
759a604cb8
commit
eda4da8aed
@ -49,7 +49,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
secondaryButton?: React.ReactNode;
|
secondaryButton?: React.ReactNode;
|
||||||
};
|
};
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
|
||||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||||
openIssuesListModal: (() => void) | null;
|
openIssuesListModal: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
|
@ -19,7 +19,7 @@ type Props = {
|
|||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
disableAddIssueOption?: boolean;
|
disableAddIssueOption?: boolean;
|
||||||
dragDisabled: boolean;
|
dragDisabled: boolean;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
|
@ -19,7 +19,12 @@ import useIssuesProperties from "hooks/use-issue-properties";
|
|||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
// components
|
// components
|
||||||
import { FiltersList, AllViews } from "components/core";
|
import { FiltersList, AllViews } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues";
|
import {
|
||||||
|
CreateUpdateIssueModal,
|
||||||
|
DeleteIssueModal,
|
||||||
|
IssuePeekOverview,
|
||||||
|
CreateUpdateDraftIssueModal,
|
||||||
|
} from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
@ -70,6 +75,9 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
// trash box
|
// trash box
|
||||||
const [trashBox, setTrashBox] = useState(false);
|
const [trashBox, setTrashBox] = useState(false);
|
||||||
|
|
||||||
|
// selected draft issue
|
||||||
|
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
@ -106,6 +114,8 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[setDeleteIssueModal, setIssueToDelete]
|
[setDeleteIssueModal, setIssueToDelete]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDraftIssueClick = (issue: any) => setSelectedDraftIssue(issue);
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
const handleOnDragEnd = useCallback(
|
||||||
async (result: DropResult) => {
|
async (result: DropResult) => {
|
||||||
setTrashBox(false);
|
setTrashBox(false);
|
||||||
@ -335,10 +345,11 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAction = useCallback(
|
const handleIssueAction = useCallback(
|
||||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||||
if (action === "copy") makeIssueCopy(issue);
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
else if (action === "edit") handleEditIssue(issue);
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
else if (action === "delete") handleDeleteIssue(issue);
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
else if (action === "updateDraft") handleDraftIssueClick(issue);
|
||||||
},
|
},
|
||||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||||
);
|
);
|
||||||
@ -451,6 +462,27 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
...preloadedData,
|
...preloadedData,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<CreateUpdateDraftIssueModal
|
||||||
|
isOpen={selectedDraftIssue !== null}
|
||||||
|
handleClose={() => setSelectedDraftIssue(null)}
|
||||||
|
data={
|
||||||
|
selectedDraftIssue
|
||||||
|
? {
|
||||||
|
...selectedDraftIssue,
|
||||||
|
is_draft: true,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
fieldsToShow={[
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"label",
|
||||||
|
"assignee",
|
||||||
|
"priority",
|
||||||
|
"dueDate",
|
||||||
|
"priority",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
|
||||||
handleClose={() => setEditIssueModal(false)}
|
handleClose={() => setEditIssueModal(false)}
|
||||||
|
@ -14,7 +14,7 @@ import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from
|
|||||||
type Props = {
|
type Props = {
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
addIssueToGroup: (groupTitle: string) => void;
|
addIssueToGroup: (groupTitle: string) => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
myIssueProjectId?: string | null;
|
myIssueProjectId?: string | null;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
|
@ -61,6 +61,7 @@ type Props = {
|
|||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
handleDraftIssueSelect?: (issue: IIssue) => void;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -82,6 +83,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
|
handleDraftIssueSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
@ -90,6 +92,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -178,6 +181,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const issuePath = isArchivedIssues
|
const issuePath = isArchivedIssues
|
||||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||||
|
: isDraftIssues
|
||||||
|
? `#`
|
||||||
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
||||||
|
|
||||||
const openPeekOverview = (issue: IIssue) => {
|
const openPeekOverview = (issue: IIssue) => {
|
||||||
@ -247,7 +252,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="truncate text-[0.825rem] text-custom-text-100"
|
className="truncate text-[0.825rem] text-custom-text-100"
|
||||||
onClick={() => openPeekOverview(issue)}
|
onClick={() => {
|
||||||
|
if (!isDraftIssues) openPeekOverview(issue);
|
||||||
|
|
||||||
|
if (handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</button>
|
</button>
|
||||||
|
@ -39,7 +39,7 @@ type Props = {
|
|||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
addIssueToGroup: () => void;
|
addIssueToGroup: () => void;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
@ -253,6 +253,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
handleDraftIssueSelect={() => handleIssueAction(issue, "updateDraft")}
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue !== null && issue.bridge_id)
|
if (removeIssue !== null && issue.bridge_id)
|
||||||
|
93
web/components/issues/confirm-issue-discard.tsx
Normal file
93
web/components/issues/confirm-issue-discard.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { SecondaryButton, PrimaryButton } from "components/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onDiscard: () => void;
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen, handleClose, onDiscard, onConfirm } = props;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await onConfirm();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||||
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
|
>
|
||||||
|
Draft Issue
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Would you like to save this issue in drafts?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-2 p-4 sm:px-6">
|
||||||
|
<div>
|
||||||
|
<SecondaryButton onClick={onDiscard}>Discard</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton onClick={handleDeletion} loading={isLoading}>
|
||||||
|
{isLoading ? "Saving..." : "Save Draft"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
580
web/components/issues/draft-issue-form.tsx
Normal file
580
web/components/issues/draft-issue-form.tsx
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
import React, { FC, useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import aiService from "services/ai.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// components
|
||||||
|
import { GptAssistantModal } from "components/core";
|
||||||
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
|
import {
|
||||||
|
IssueAssigneeSelect,
|
||||||
|
IssueDateSelect,
|
||||||
|
IssueEstimateSelect,
|
||||||
|
IssueLabelSelect,
|
||||||
|
IssuePrioritySelect,
|
||||||
|
IssueProjectSelect,
|
||||||
|
IssueStateSelect,
|
||||||
|
} from "components/issues/select";
|
||||||
|
import { CreateStateModal } from "components/states";
|
||||||
|
import { CreateLabelModal } from "components/labels";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
|
import { TipTapEditor } from "components/tiptap";
|
||||||
|
// icons
|
||||||
|
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssue> = {
|
||||||
|
project: "",
|
||||||
|
name: "",
|
||||||
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description_html: "<p></p>",
|
||||||
|
estimate_point: null,
|
||||||
|
state: "",
|
||||||
|
parent: null,
|
||||||
|
priority: "none",
|
||||||
|
assignees: [],
|
||||||
|
assignees_list: [],
|
||||||
|
labels: [],
|
||||||
|
labels_list: [],
|
||||||
|
start_date: null,
|
||||||
|
target_date: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IssueFormProps {
|
||||||
|
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
|
||||||
|
data?: Partial<IIssue> | null;
|
||||||
|
prePopulatedData?: Partial<IIssue> | null;
|
||||||
|
projectId: string;
|
||||||
|
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
createMore: boolean;
|
||||||
|
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handleClose: () => void;
|
||||||
|
status: boolean;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
fieldsToShow: (
|
||||||
|
| "project"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "state"
|
||||||
|
| "priority"
|
||||||
|
| "assignee"
|
||||||
|
| "label"
|
||||||
|
| "startDate"
|
||||||
|
| "dueDate"
|
||||||
|
| "estimate"
|
||||||
|
| "parent"
|
||||||
|
| "all"
|
||||||
|
)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||||
|
const {
|
||||||
|
handleFormSubmit,
|
||||||
|
data,
|
||||||
|
prePopulatedData,
|
||||||
|
projectId,
|
||||||
|
setActiveProject,
|
||||||
|
createMore,
|
||||||
|
setCreateMore,
|
||||||
|
handleClose,
|
||||||
|
status,
|
||||||
|
user,
|
||||||
|
fieldsToShow,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [stateModal, setStateModal] = useState(false);
|
||||||
|
const [labelModal, setLabelModal] = useState(false);
|
||||||
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||||
|
|
||||||
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
control,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
setFocus,
|
||||||
|
} = useForm<IIssue>({
|
||||||
|
defaultValues: prePopulatedData ?? defaultValues,
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueName = watch("name");
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateUpdateIssue = async (
|
||||||
|
formData: Partial<IIssue>,
|
||||||
|
action: "saveDraft" | "createToNewIssue" = "saveDraft"
|
||||||
|
) => {
|
||||||
|
await handleFormSubmit({
|
||||||
|
...formData,
|
||||||
|
is_draft: action === "saveDraft",
|
||||||
|
});
|
||||||
|
|
||||||
|
setGptAssistantModal(false);
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
project: projectId,
|
||||||
|
description: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description_html: "<p></p>",
|
||||||
|
});
|
||||||
|
editorRef?.current?.clearEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAiAssistance = async (response: string) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setValue("description", {});
|
||||||
|
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||||
|
editorRef.current?.setEditorValue(`${watch("description_html")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoGenerateDescription = async () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setIAmFeelingLucky(true);
|
||||||
|
|
||||||
|
aiService
|
||||||
|
.createGptTask(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
{
|
||||||
|
prompt: issueName,
|
||||||
|
task: "Generate a proper description for this issue.",
|
||||||
|
},
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.response === "")
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
"Issue title isn't informative enough to generate the description. Please try with a different title.",
|
||||||
|
});
|
||||||
|
else handleAiAssistance(res.response_html);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error = err?.data?.error;
|
||||||
|
|
||||||
|
if (err.status === 429)
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message:
|
||||||
|
error ||
|
||||||
|
"You have reached the maximum number of requests of 50 requests per month per user.",
|
||||||
|
});
|
||||||
|
else
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: error || "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => setIAmFeelingLucky(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
...(prePopulatedData ?? {}),
|
||||||
|
...(data ?? {}),
|
||||||
|
});
|
||||||
|
}, [setFocus, prePopulatedData, reset, data]);
|
||||||
|
|
||||||
|
// update projectId in form when projectId changes
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
...getValues(),
|
||||||
|
project: projectId,
|
||||||
|
});
|
||||||
|
}, [getValues, projectId, reset]);
|
||||||
|
|
||||||
|
const startDate = watch("start_date");
|
||||||
|
const targetDate = watch("target_date");
|
||||||
|
|
||||||
|
const minDate = startDate ? new Date(startDate) : null;
|
||||||
|
minDate?.setDate(minDate.getDate());
|
||||||
|
|
||||||
|
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||||
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{projectId && (
|
||||||
|
<>
|
||||||
|
<CreateStateModal
|
||||||
|
isOpen={stateModal}
|
||||||
|
handleClose={() => setStateModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<CreateLabelModal
|
||||||
|
isOpen={labelModal}
|
||||||
|
handleClose={() => setLabelModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
user={user}
|
||||||
|
onSuccess={(response) => {
|
||||||
|
setValue("labels", [...watch("labels"), response.id]);
|
||||||
|
setValue("labels_list", [...watch("labels_list"), response.id]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
|
||||||
|
>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueProjectSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
onChange(val);
|
||||||
|
setActiveProject(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
|
||||||
|
{status ? "Update" : "Create"} Issue
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{watch("parent") &&
|
||||||
|
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
|
||||||
|
selectedParentIssue && (
|
||||||
|
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="block h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: selectedParentIssue.state__color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="flex-shrink-0 text-custom-text-200">
|
||||||
|
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||||
|
</span>
|
||||||
|
<span className="truncate font-medium">
|
||||||
|
{selectedParentIssue.name.substring(0, 50)}
|
||||||
|
</span>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-3 w-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setValue("parent", null);
|
||||||
|
setSelectedParentIssue(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
className="resize-none text-xl"
|
||||||
|
placeholder="Title"
|
||||||
|
autoComplete="off"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Title is required",
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: "Title should be less than 255 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{issueName && issueName !== "" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
||||||
|
iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
|
}`}
|
||||||
|
onClick={handleAutoGenerateDescription}
|
||||||
|
disabled={iAmFeelingLucky}
|
||||||
|
>
|
||||||
|
{iAmFeelingLucky ? (
|
||||||
|
"Generating response..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</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)}
|
||||||
|
>
|
||||||
|
<SparklesIcon className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="description_html"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
if (!value && !watch("description_html")) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TipTapEditor
|
||||||
|
workspaceSlug={workspaceSlug as string}
|
||||||
|
ref={editorRef}
|
||||||
|
debouncedUpdatesEnabled={false}
|
||||||
|
value={
|
||||||
|
!value ||
|
||||||
|
value === "" ||
|
||||||
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
? watch("description_html")
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
customClassName="min-h-[150px]"
|
||||||
|
onChange={(description: Object, description_html: string) => {
|
||||||
|
onChange(description_html);
|
||||||
|
setValue("description", description);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<GptAssistantModal
|
||||||
|
isOpen={gptAssistantModal}
|
||||||
|
handleClose={() => {
|
||||||
|
setGptAssistantModal(false);
|
||||||
|
// this is done so that the title do not reset after gpt popover closed
|
||||||
|
reset(getValues());
|
||||||
|
}}
|
||||||
|
inset="top-2 left-0"
|
||||||
|
content=""
|
||||||
|
htmlContent={watch("description_html")}
|
||||||
|
onResponse={(response) => {
|
||||||
|
handleAiAssistance(response);
|
||||||
|
}}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="state"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueStateSelect
|
||||||
|
setIsOpen={setStateModal}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssuePrioritySelect value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="assignees"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueAssigneeSelect
|
||||||
|
projectId={projectId}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="labels"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueLabelSelect
|
||||||
|
setIsOpen={setLabelModal}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="start_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueDateSelect
|
||||||
|
label="Start date"
|
||||||
|
maxDate={maxDate ?? undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="target_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueDateSelect
|
||||||
|
label="Due date"
|
||||||
|
minDate={minDate ?? undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estimate_point"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssueEstimateSelect value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="parent"
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<ParentIssuesListModal
|
||||||
|
isOpen={parentIssueListModalOpen}
|
||||||
|
handleClose={() => setParentIssueListModalOpen(false)}
|
||||||
|
onChange={(issue) => {
|
||||||
|
onChange(issue.id);
|
||||||
|
setSelectedParentIssue(issue);
|
||||||
|
}}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
{watch("parent") ? (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
Change parent issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setValue("parent", null)}
|
||||||
|
>
|
||||||
|
Remove parent issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
Select Parent Issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-1"
|
||||||
|
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<span className="text-xs">Create more</span>
|
||||||
|
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Saving..." : "Save Draft"}
|
||||||
|
</SecondaryButton>
|
||||||
|
{data && (
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Add Issue"}
|
||||||
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
285
web/components/issues/draft-issue-modal.tsx
Normal file
285
web/components/issues/draft-issue-modal.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
|
// components
|
||||||
|
import { DraftIssueForm } from "components/issues";
|
||||||
|
// types
|
||||||
|
import type { IIssue } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
PROJECT_ISSUES_DETAILS,
|
||||||
|
USER_ISSUE,
|
||||||
|
SUB_ISSUES,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
interface IssuesModalProps {
|
||||||
|
data?: IIssue | null;
|
||||||
|
handleClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
isUpdatingSingleIssue?: boolean;
|
||||||
|
prePopulateData?: Partial<IIssue>;
|
||||||
|
fieldsToShow?: (
|
||||||
|
| "project"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "state"
|
||||||
|
| "priority"
|
||||||
|
| "assignee"
|
||||||
|
| "label"
|
||||||
|
| "startDate"
|
||||||
|
| "dueDate"
|
||||||
|
| "estimate"
|
||||||
|
| "parent"
|
||||||
|
| "all"
|
||||||
|
)[];
|
||||||
|
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||||
|
data,
|
||||||
|
handleClose,
|
||||||
|
isOpen,
|
||||||
|
isUpdatingSingleIssue = false,
|
||||||
|
prePopulateData,
|
||||||
|
fieldsToShow = ["all"],
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
// states
|
||||||
|
const [createMore, setCreateMore] = useState(false);
|
||||||
|
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const { displayFilters, params } = useIssuesView();
|
||||||
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
|
const { ...viewGanttParams } = params;
|
||||||
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
|
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
|
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
|
||||||
|
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
|
||||||
|
prePopulateData = {
|
||||||
|
...prePopulateData,
|
||||||
|
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if modal is closed, reset active project to null
|
||||||
|
// and return to avoid activeProject being set to some other project
|
||||||
|
if (!isOpen) {
|
||||||
|
setActiveProject(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if data is present, set active project to the project of the
|
||||||
|
// issue. This has more priority than the project in the url.
|
||||||
|
if (data && data.project) {
|
||||||
|
setActiveProject(data.project);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if data is not present, set active project to the project
|
||||||
|
// in the url. This has the least priority.
|
||||||
|
if (projects && projects.length > 0 && !activeProject)
|
||||||
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
|
}, [activeProject, data, projectId, projects, isOpen]);
|
||||||
|
|
||||||
|
const calendarFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
|
||||||
|
|
||||||
|
const spreadsheetFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
|
||||||
|
|
||||||
|
const ganttFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
|
||||||
|
|
||||||
|
const createIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||||
|
.then(async () => {
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
|
||||||
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (displayFilters.layout === "gantt_chart")
|
||||||
|
mutate(ganttFetchKey, {
|
||||||
|
start_target_date: true,
|
||||||
|
order_by: "sort_order",
|
||||||
|
});
|
||||||
|
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issue created successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||||
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
|
|
||||||
|
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createMore) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
||||||
|
.then((res) => {
|
||||||
|
if (isUpdatingSingleIssue) {
|
||||||
|
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||||
|
} else {
|
||||||
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createMore) onClose();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issue updated successfully.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be updated. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !activeProject) return;
|
||||||
|
|
||||||
|
const payload: Partial<IIssue> = {
|
||||||
|
...formData,
|
||||||
|
assignees_list: formData.assignees ?? [],
|
||||||
|
labels_list: formData.labels ?? [],
|
||||||
|
description: formData.description ?? "",
|
||||||
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data) await createIssue(payload);
|
||||||
|
else await updateIssue(payload);
|
||||||
|
|
||||||
|
if (onSubmit) await onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projects || projects.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
|
<DraftIssueForm
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
prePopulatedData={prePopulateData}
|
||||||
|
data={data}
|
||||||
|
createMore={createMore}
|
||||||
|
setCreateMore={setCreateMore}
|
||||||
|
handleClose={onClose}
|
||||||
|
projectId={activeProject ?? ""}
|
||||||
|
setActiveProject={setActiveProject}
|
||||||
|
status={data ? true : false}
|
||||||
|
user={user}
|
||||||
|
fieldsToShow={fieldsToShow}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import aiService from "services/ai.service";
|
import aiService from "services/ai.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
@ -62,8 +63,11 @@ export interface IssueFormProps {
|
|||||||
createMore: boolean;
|
createMore: boolean;
|
||||||
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
|
handleDiscardClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handleFormDirty: (payload: Partial<IIssue> | null) => void;
|
||||||
fieldsToShow: (
|
fieldsToShow: (
|
||||||
| "project"
|
| "project"
|
||||||
| "name"
|
| "name"
|
||||||
@ -80,18 +84,21 @@ export interface IssueFormProps {
|
|||||||
)[];
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueForm: FC<IssueFormProps> = ({
|
export const IssueForm: FC<IssueFormProps> = (props) => {
|
||||||
|
const {
|
||||||
handleFormSubmit,
|
handleFormSubmit,
|
||||||
initialData,
|
initialData,
|
||||||
projectId,
|
projectId,
|
||||||
setActiveProject,
|
setActiveProject,
|
||||||
createMore,
|
createMore,
|
||||||
setCreateMore,
|
setCreateMore,
|
||||||
handleClose,
|
handleDiscardClose,
|
||||||
status,
|
status,
|
||||||
user,
|
user,
|
||||||
fieldsToShow,
|
fieldsToShow,
|
||||||
}) => {
|
handleFormDirty,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [stateModal, setStateModal] = useState(false);
|
const [stateModal, setStateModal] = useState(false);
|
||||||
const [labelModal, setLabelModal] = useState(false);
|
const [labelModal, setLabelModal] = useState(false);
|
||||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
@ -100,6 +107,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
|
|
||||||
|
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -109,7 +118,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
@ -124,6 +133,17 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: getValues("name"),
|
||||||
|
description: getValues("description"),
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDirty) handleFormDirty(payload);
|
||||||
|
else handleFormDirty(null);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [JSON.stringify(payload), isDirty]);
|
||||||
|
|
||||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -543,7 +563,15 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
const data = JSON.stringify(getValues());
|
||||||
|
setValueInLocalStorage(data);
|
||||||
|
handleDiscardClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</SecondaryButton>
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
{status
|
{status
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
|
@ -16,3 +16,6 @@ export * from "./sub-issues-list";
|
|||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./peek-overview";
|
export * from "./peek-overview";
|
||||||
|
export * from "./confirm-issue-discard";
|
||||||
|
export * from "./draft-issue-form";
|
||||||
|
export * from "./draft-issue-modal";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
// components
|
// components
|
||||||
import { IssueForm } from "components/issues";
|
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -35,6 +35,7 @@ import {
|
|||||||
MODULE_DETAILS,
|
MODULE_DETAILS,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
INBOX_ISSUES,
|
INBOX_ISSUES,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||||
@ -73,6 +74,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// states
|
// states
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
|
const [formDirtyState, setFormDirtyState] = useState<any>(null);
|
||||||
|
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
|
||||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -80,7 +83,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
const { displayFilters, params } = useIssuesView();
|
const { displayFilters, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
const { order_by, group_by, ...viewGanttParams } = params;
|
const { ...viewGanttParams } = params;
|
||||||
const { params: inboxParams } = useInboxView();
|
const { params: inboxParams } = useInboxView();
|
||||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
@ -99,10 +102,23 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = () => {
|
||||||
|
if (formDirtyState !== null) {
|
||||||
|
setShowConfirmDiscard(true);
|
||||||
|
} else {
|
||||||
handleClose();
|
handleClose();
|
||||||
setActiveProject(null);
|
setActiveProject(null);
|
||||||
}, [handleClose]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDiscardClose = () => {
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormDirty = (data: any) => {
|
||||||
|
setFormDirtyState(data);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if modal is closed, reset active project to null
|
// if modal is closed, reset active project to null
|
||||||
@ -275,10 +291,50 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createMore) onClose();
|
if (!createMore) onDiscardClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDraftIssue = async () => {
|
||||||
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
|
const payload: Partial<IIssue> = {
|
||||||
|
...formDirtyState,
|
||||||
|
};
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Draft Issue created successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
setFormDirtyState(null);
|
||||||
|
setShowConfirmDiscard(false);
|
||||||
|
|
||||||
|
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||||
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
|
|
||||||
|
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Issue could not be created. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateIssue = async (payload: Partial<IIssue>) => {
|
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -294,7 +350,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||||
|
|
||||||
if (!createMore) onClose();
|
if (!createMore) onDiscardClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -331,6 +387,19 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
if (!projects || projects.length === 0) return null;
|
if (!projects || projects.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ConfirmIssueDiscard
|
||||||
|
isOpen={showConfirmDiscard}
|
||||||
|
handleClose={() => setShowConfirmDiscard(false)}
|
||||||
|
onConfirm={createDraftIssue}
|
||||||
|
onDiscard={() => {
|
||||||
|
handleClose();
|
||||||
|
setActiveProject(null);
|
||||||
|
setFormDirtyState(null);
|
||||||
|
setShowConfirmDiscard(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@ -363,11 +432,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
createMore={createMore}
|
createMore={createMore}
|
||||||
setCreateMore={setCreateMore}
|
setCreateMore={setCreateMore}
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
|
handleDiscardClose={onDiscardClose}
|
||||||
|
setIsConfirmDiscardOpen={setShowConfirmDiscard}
|
||||||
projectId={activeProject ?? ""}
|
projectId={activeProject ?? ""}
|
||||||
setActiveProject={setActiveProject}
|
setActiveProject={setActiveProject}
|
||||||
status={data ? true : false}
|
status={data ? true : false}
|
||||||
user={user}
|
user={user}
|
||||||
fieldsToShow={fieldsToShow}
|
fieldsToShow={fieldsToShow}
|
||||||
|
handleFormDirty={handleFormDirty}
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
@ -375,5 +447,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -205,7 +205,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAction = useCallback(
|
const handleIssueAction = useCallback(
|
||||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||||
if (action === "copy") makeIssueCopy(issue);
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
else if (action === "edit") handleEditIssue(issue);
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
else if (action === "delete") handleDeleteIssue(issue);
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
@ -204,7 +204,7 @@ export const ProfileIssuesView = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleIssueAction = useCallback(
|
const handleIssueAction = useCallback(
|
||||||
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
|
||||||
if (action === "copy") makeIssueCopy(issue);
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
else if (action === "edit") handleEditIssue(issue);
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
else if (action === "delete") handleDeleteIssue(issue);
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
PhotoFilterOutlined,
|
PhotoFilterOutlined,
|
||||||
SettingsOutlined,
|
SettingsOutlined,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import { PenSquare } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
@ -288,6 +289,16 @@ export const SingleSidebarProject: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/${workspaceSlug}/projects/${project?.id}/draft-issues`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<PenSquare className="!text-base !leading-4 w-[14px] h-[14px] text-custom-text-300" />
|
||||||
|
<span>Draft Issues</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
|
||||||
>
|
>
|
||||||
|
@ -1,34 +1,128 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { Icon } from "components/ui";
|
import { Icon } from "components/ui";
|
||||||
|
import { ChevronDown, PenSquare } from "lucide-react";
|
||||||
|
// headless ui
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
// components
|
||||||
|
import { CreateUpdateDraftIssueModal } from "components/issues";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export const WorkspaceSidebarQuickAction = () => {
|
export const WorkspaceSidebarQuickAction = () => {
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
|
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateDraftIssueModal
|
||||||
|
isOpen={isDraftIssueModalOpen}
|
||||||
|
handleClose={() => setIsDraftIssueModalOpen(false)}
|
||||||
|
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
|
||||||
|
onSubmit={() => {
|
||||||
|
localStorage.removeItem("draftedIssue");
|
||||||
|
clearValue();
|
||||||
|
setIsDraftIssueModalOpen(false);
|
||||||
|
}}
|
||||||
|
fieldsToShow={[
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"label",
|
||||||
|
"assignee",
|
||||||
|
"priority",
|
||||||
|
"dueDate",
|
||||||
|
"priority",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
||||||
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
|
className={`flex items-center justify-between w-full rounded cursor-pointer px-4 gap-1 ${
|
||||||
store?.theme?.sidebarCollapsed
|
store?.theme?.sidebarCollapsed
|
||||||
? "px-2 hover:bg-custom-sidebar-background-80"
|
? "px-2 hover:bg-custom-sidebar-background-80"
|
||||||
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
||||||
}`}
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
document.dispatchEvent(e);
|
document.dispatchEvent(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon iconName="edit_square" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
<Icon
|
||||||
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
|
iconName="edit_square"
|
||||||
|
className="!text-lg !leading-4 text-custom-sidebar-text-300"
|
||||||
|
/>
|
||||||
|
{!store?.theme?.sidebarCollapsed && (
|
||||||
|
<span className="text-sm font-medium">New Issue</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
|
||||||
|
|
||||||
|
{storedValue && (
|
||||||
|
<div className="relative">
|
||||||
|
<Menu as={React.Fragment}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
||||||
|
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute -right-4 mt-1 w-52 bg-custom-background-300">
|
||||||
|
<div className="px-1 py-1 ">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||||
|
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
|
||||||
|
>
|
||||||
|
<PenSquare
|
||||||
|
size={16}
|
||||||
|
className="!text-lg !leading-4 text-custom-sidebar-text-300 mx-2"
|
||||||
|
/>
|
||||||
|
Last Drafted Issue
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
|
||||||
store?.theme?.sidebarCollapsed
|
store?.theme?.sidebarCollapsed
|
||||||
@ -43,5 +137,6 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -140,6 +140,15 @@ export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, para
|
|||||||
|
|
||||||
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
|
||||||
|
if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
|
const paramsKey = paramsToKey(params);
|
||||||
|
|
||||||
|
return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||||
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
@ -38,6 +39,7 @@ const useIssuesView = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
order_by: displayFilters?.order_by,
|
order_by: displayFilters?.order_by,
|
||||||
@ -72,6 +74,15 @@ const useIssuesView = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: draftIssues, mutate: mutateDraftIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
|
||||||
|
? PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
|
||||||
|
? () => issuesService.getDraftIssues(workspaceSlug as string, projectId as string, params)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId && params
|
workspaceSlug && projectId && cycleId && params
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||||
@ -151,6 +162,8 @@ const useIssuesView = () => {
|
|||||||
? viewIssues
|
? viewIssues
|
||||||
: isArchivedIssues
|
: isArchivedIssues
|
||||||
? projectArchivedIssues
|
? projectArchivedIssues
|
||||||
|
: isDraftIssues
|
||||||
|
? draftIssues
|
||||||
: projectIssues;
|
: projectIssues;
|
||||||
|
|
||||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||||
@ -169,6 +182,8 @@ const useIssuesView = () => {
|
|||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
isArchivedIssues,
|
isArchivedIssues,
|
||||||
|
isDraftIssues,
|
||||||
|
draftIssues,
|
||||||
emptyStatesObject,
|
emptyStatesObject,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -191,6 +206,8 @@ const useIssuesView = () => {
|
|||||||
? mutateViewIssues
|
? mutateViewIssues
|
||||||
: isArchivedIssues
|
: isArchivedIssues
|
||||||
? mutateProjectArchivedIssues
|
? mutateProjectArchivedIssues
|
||||||
|
: isDraftIssues
|
||||||
|
? mutateDraftIssues
|
||||||
: mutateProjectIssues,
|
: mutateProjectIssues,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// layouts
|
||||||
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
|
// contexts
|
||||||
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
|
// helper
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
// components
|
||||||
|
import { IssuesFilterView, IssuesView } from "components/core";
|
||||||
|
// ui
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
// icons
|
||||||
|
import { X, PenSquare } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import type { NextPage } from "next";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const ProjectDraftIssues: NextPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: projectDetails } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueViewContextProvider>
|
||||||
|
<ProjectAuthorizationWrapper
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`}
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IssuesFilterView />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||||
|
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<PenSquare className="h-3 w-3 text-custom-text-300" />
|
||||||
|
<span>Draft Issues</span>
|
||||||
|
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<IssuesView />
|
||||||
|
</div>
|
||||||
|
</ProjectAuthorizationWrapper>
|
||||||
|
</IssueViewContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectDraftIssues;
|
@ -617,6 +617,68 @@ class ProjectIssuesServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProjectIssuesServices();
|
async createDraftIssue(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
data: any,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): Promise<any> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDraftIssue(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
data: any,
|
||||||
|
user: ICurrentUserResponse
|
||||||
|
): Promise<any> {
|
||||||
|
return this.patch(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||||
|
return this.delete(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectIssuesServices = new ProjectIssuesServices();
|
||||||
|
|
||||||
|
export default projectIssuesServices;
|
||||||
|
1
web/types/issues.d.ts
vendored
1
web/types/issues.d.ts
vendored
@ -118,6 +118,7 @@ export interface IIssue {
|
|||||||
issue_module: IIssueModule | null;
|
issue_module: IIssueModule | null;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
label_details: any[];
|
label_details: any[];
|
||||||
|
is_draft: boolean;
|
||||||
labels_list: string[];
|
labels_list: string[];
|
||||||
links_list: IIssueLink[];
|
links_list: IIssueLink[];
|
||||||
link_count: number;
|
link_count: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user