forked from github/plane
[WEB-581] fix: issue editing functionality enhancement in Create/Edit modal (#3809)
* chore: draft issue update request * chore: changed the serializer * chore: handled issue description in issue modal, inbox issues mutation and draft issue mutaion and changed the endpoints * chore: handled draft toggle in make a issue payload in issues * chore: handled issue labels in the inbox issues --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
c858b76054
commit
34d6b135f2
@ -2310,17 +2310,10 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if request.data.get(
|
serializer.save()
|
||||||
"is_draft"
|
|
||||||
) is not None and not request.data.get("is_draft"):
|
|
||||||
serializer.save(
|
|
||||||
created_at=timezone.now(), updated_at=timezone.now()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
serializer.save()
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue_draft.activity.updated",
|
type="issue_draft.activity.updated",
|
||||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||||
@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
<DayPicker
|
<DayPicker
|
||||||
selected={date ? new Date(date) : undefined}
|
selected={date ? new Date(date) : undefined}
|
||||||
defaultMonth={date ? new Date(date) : undefined}
|
defaultMonth={date ? new Date(date) : undefined}
|
||||||
onSelect={(date) => { if (!date) return; setDate(date) }}
|
onSelect={(date) => {
|
||||||
|
if (!date) return;
|
||||||
|
setDate(date);
|
||||||
|
}}
|
||||||
mode="single"
|
mode="single"
|
||||||
className="border border-custom-border-200 rounded-md p-3"
|
className="border border-custom-border-200 rounded-md p-3"
|
||||||
disabled={[{
|
disabled={[
|
||||||
before: tomorrow,
|
{
|
||||||
}]}
|
before: tomorrow,
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
@ -54,7 +54,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
|||||||
showToast: boolean = true
|
showToast: boolean = true
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Issue updated successfully",
|
title: "Issue updated successfully",
|
||||||
@ -64,7 +64,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
|||||||
}
|
}
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Inbox issue updated",
|
eventName: "Inbox issue updated",
|
||||||
payload: { ...response, state: "SUCCESS", element: "Inbox" },
|
payload: { ...data, state: "SUCCESS", element: "Inbox" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
change_details: Object.values(data).join(","),
|
change_details: Object.values(data).join(","),
|
||||||
|
@ -154,6 +154,10 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={!is_editable}
|
disabled={!is_editable}
|
||||||
|
isInboxIssue
|
||||||
|
onLabelUpdate={(val: string[]) =>
|
||||||
|
issueOperations.update(workspaceSlug, projectId, issueId, { label_ids: val })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,8 @@ export type TIssueLabel = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
isInboxIssue?: boolean;
|
||||||
|
onLabelUpdate?: (labelIds: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLabelOperations = {
|
export type TLabelOperations = {
|
||||||
@ -21,7 +23,7 @@ export type TLabelOperations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { updateIssue } = useIssueDetail();
|
const { updateIssue } = useIssueDetail();
|
||||||
const { createLabel } = useLabel();
|
const { createLabel } = useLabel();
|
||||||
@ -31,12 +33,14 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
() => ({
|
() => ({
|
||||||
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
|
||||||
setToastAlert({
|
else await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
title: "Issue updated successfully",
|
if (!isInboxIssue)
|
||||||
type: "success",
|
setToastAlert({
|
||||||
message: "Issue updated successfully",
|
title: "Issue updated successfully",
|
||||||
});
|
type: "success",
|
||||||
|
message: "Issue updated successfully",
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Issue update failed",
|
title: "Issue update failed",
|
||||||
@ -48,11 +52,12 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
||||||
try {
|
try {
|
||||||
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
||||||
setToastAlert({
|
if (!isInboxIssue)
|
||||||
title: "Label created successfully",
|
setToastAlert({
|
||||||
type: "success",
|
title: "Label created successfully",
|
||||||
message: "Label created successfully",
|
type: "success",
|
||||||
});
|
message: "Label created successfully",
|
||||||
|
});
|
||||||
return labelResponse;
|
return labelResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[updateIssue, createLabel, setToastAlert]
|
[updateIssue, createLabel, setToastAlert, onLabelUpdate]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||||
|
|
||||||
const duplicateIssuePayload = omit(
|
const duplicateIssuePayload = omit(
|
||||||
{
|
{
|
||||||
...issue,
|
...issue,
|
||||||
name: `${issue.name} (copy)`,
|
name: `${issue.name} (copy)`,
|
||||||
|
is_draft: isDraftIssue ? false : issue.is_draft,
|
||||||
},
|
},
|
||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
|
@ -60,7 +60,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
<DraftKanBanLayout />
|
<DraftKanBanLayout />
|
||||||
) : null}
|
) : null}
|
||||||
{/* issue peek overview */}
|
{/* issue peek overview */}
|
||||||
<IssuePeekOverview />
|
<IssuePeekOverview is_draft />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
StateDropdown,
|
StateDropdown,
|
||||||
} from "components/dropdowns";
|
} from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.description_html) setValue("description_html", data?.description_html);
|
||||||
|
}, [data?.description_html]);
|
||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
||||||
@ -365,80 +369,105 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
{data?.description_html === undefined ? (
|
||||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
<Loader className="min-h-[7rem] space-y-2 py-2 border border-custom-border-200 rounded-md p-2 overflow-hidden">
|
||||||
<button
|
<Loader.Item width="100%" height="26px" />
|
||||||
type="button"
|
<div className="flex items-center gap-2">
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
|
<Loader.Item width="26px" height="26px" />
|
||||||
iAmFeelingLucky ? "cursor-wait" : ""
|
<Loader.Item width="400px" height="26px" />
|
||||||
}`}
|
</div>
|
||||||
onClick={handleAutoGenerateDescription}
|
<div className="flex items-center gap-2">
|
||||||
disabled={iAmFeelingLucky}
|
<Loader.Item width="26px" height="26px" />
|
||||||
tabIndex={getTabIndex("feeling_lucky")}
|
<Loader.Item width="400px" height="26px" />
|
||||||
>
|
</div>
|
||||||
{iAmFeelingLucky ? (
|
<Loader.Item width="80%" height="26px" />
|
||||||
"Generating response"
|
<div className="flex items-center gap-2">
|
||||||
) : (
|
<Loader.Item width="50%" height="26px" />
|
||||||
<>
|
</div>
|
||||||
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||||
</>
|
<Loader.Item width="100px" height="26px" />
|
||||||
)}
|
<Loader.Item width="50px" height="26px" />
|
||||||
</button>
|
</div>
|
||||||
)}
|
</Loader>
|
||||||
{envConfig?.has_openai_configured && (
|
) : (
|
||||||
<GptAssistantPopover
|
<Fragment>
|
||||||
isOpen={gptAssistantModal}
|
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||||
projectId={projectId}
|
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||||
handleClose={() => {
|
|
||||||
setGptAssistantModal((prevData) => !prevData);
|
|
||||||
// this is done so that the title do not reset after gpt popover closed
|
|
||||||
reset(getValues());
|
|
||||||
}}
|
|
||||||
onResponse={(response) => {
|
|
||||||
handleAiAssistance(response);
|
|
||||||
}}
|
|
||||||
placement="top-end"
|
|
||||||
button={
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
|
||||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
tabIndex={getTabIndex("ai_assistant")}
|
}`}
|
||||||
|
onClick={handleAutoGenerateDescription}
|
||||||
|
disabled={iAmFeelingLucky}
|
||||||
|
tabIndex={getTabIndex("feeling_lucky")}
|
||||||
>
|
>
|
||||||
<Sparkle className="h-4 w-4" />
|
{iAmFeelingLucky ? (
|
||||||
AI
|
"Generating response"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
|
{envConfig?.has_openai_configured && (
|
||||||
|
<GptAssistantPopover
|
||||||
|
isOpen={gptAssistantModal}
|
||||||
|
projectId={projectId}
|
||||||
|
handleClose={() => {
|
||||||
|
setGptAssistantModal((prevData) => !prevData);
|
||||||
|
// this is done so that the title do not reset after gpt popover closed
|
||||||
|
reset(getValues());
|
||||||
|
}}
|
||||||
|
onResponse={(response) => {
|
||||||
|
handleAiAssistance(response);
|
||||||
|
}}
|
||||||
|
placement="top-end"
|
||||||
|
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)}
|
||||||
|
tabIndex={getTabIndex("ai_assistant")}
|
||||||
|
>
|
||||||
|
<Sparkle className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="description_html"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
|
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||||
|
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||||
|
ref={editorRef}
|
||||||
|
debouncedUpdatesEnabled={false}
|
||||||
|
value={
|
||||||
|
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||||
|
? watch("description_html")
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
initialValue={data?.description_html}
|
||||||
|
customClassName="min-h-[7rem] border-custom-border-100"
|
||||||
|
onChange={(description: Object, description_html: string) => {
|
||||||
|
onChange(description_html);
|
||||||
|
handleFormChange();
|
||||||
|
}}
|
||||||
|
mentionHighlights={mentionHighlights}
|
||||||
|
mentionSuggestions={mentionSuggestions}
|
||||||
|
// tabIndex={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</Fragment>
|
||||||
</div>
|
)}
|
||||||
<Controller
|
|
||||||
name="description_html"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<RichTextEditorWithRef
|
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
|
||||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
|
||||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
|
||||||
ref={editorRef}
|
|
||||||
debouncedUpdatesEnabled={false}
|
|
||||||
value={
|
|
||||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
|
||||||
? watch("description_html")
|
|
||||||
: value
|
|
||||||
}
|
|
||||||
customClassName="min-h-[7rem] border-custom-border-100"
|
|
||||||
onChange={(description: Object, description_html: string) => {
|
|
||||||
onChange(description_html);
|
|
||||||
handleFormChange();
|
|
||||||
}}
|
|
||||||
mentionHighlights={mentionHighlights}
|
|
||||||
mentionSuggestions={mentionSuggestions}
|
|
||||||
// tabIndex={2}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -3,7 +3,16 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useCycle, useIssues, useModule, useProject, useWorkspace } from "hooks/store";
|
import {
|
||||||
|
useApplication,
|
||||||
|
useEventTracker,
|
||||||
|
useCycle,
|
||||||
|
useIssues,
|
||||||
|
useModule,
|
||||||
|
useProject,
|
||||||
|
useWorkspace,
|
||||||
|
useIssueDetail,
|
||||||
|
} from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
@ -39,6 +48,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
||||||
|
const [description, setDescription] = useState<string | undefined>(undefined);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
||||||
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||||
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
||||||
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT);
|
const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
|
||||||
|
const { fetchIssue } = useIssueDetail();
|
||||||
// store mapping based on current store
|
// store mapping based on current store
|
||||||
const issueStores = {
|
const issueStores = {
|
||||||
[EIssuesStoreType.PROJECT]: {
|
[EIssuesStoreType.PROJECT]: {
|
||||||
@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
// current store details
|
// current store details
|
||||||
const { store: currentIssueStore, viewId } = issueStores[storeType];
|
const { store: currentIssueStore, viewId } = issueStores[storeType];
|
||||||
|
|
||||||
|
const fetchIssueDetail = async (issueId: string | undefined) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
if (issueId === undefined) {
|
||||||
|
setDescription("<p></p>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT");
|
||||||
|
if (response) setDescription(response?.description_html || "<p></p>");
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// fetching issue details
|
||||||
|
if (isOpen) fetchIssueDetail(data?.id);
|
||||||
|
|
||||||
// if modal is closed, reset active project to null
|
// if modal is closed, reset active project to null
|
||||||
// and return to avoid activeProjectId being set to some other project
|
// and return to avoid activeProjectId being set to some other project
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
// in the url. This has the least priority.
|
// in the url. This has the least priority.
|
||||||
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
|
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
|
||||||
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
|
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
|
||||||
|
|
||||||
|
// clearing up the description state when we leave the component
|
||||||
|
return () => setDescription(undefined);
|
||||||
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
|
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
|
||||||
|
|
||||||
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
|
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
|
||||||
@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = is_draft_issue
|
const response = is_draft_issue
|
||||||
? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload)
|
? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload)
|
||||||
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
||||||
if (!response) throw new Error();
|
if (!response) throw new Error();
|
||||||
|
|
||||||
@ -183,7 +210,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
|
isDraft
|
||||||
|
? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload)
|
||||||
|
: await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
changesMade={changesMade}
|
changesMade={changesMade}
|
||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
|
description_html: description,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
@ -276,6 +307,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
<IssueFormRoot
|
<IssueFormRoot
|
||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
|
description_html: description,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
|
@ -15,6 +15,7 @@ import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
|||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
|
is_draft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TIssuePeekOperations = {
|
export type TIssuePeekOperations = {
|
||||||
@ -45,7 +46,7 @@ export type TIssuePeekOperations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
const { is_archived = false } = props;
|
const { is_archived = false, is_draft = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// router
|
// router
|
||||||
@ -72,7 +73,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
() => ({
|
() => ({
|
||||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
|
await fetchIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching the parent issue");
|
console.error("Error fetching the parent issue");
|
||||||
}
|
}
|
||||||
@ -302,6 +308,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
is_archived,
|
is_archived,
|
||||||
|
is_draft,
|
||||||
fetchIssue,
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
|
@ -42,7 +42,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && projectId && archivedIssueId
|
workspaceSlug && projectId && archivedIssueId
|
||||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true)
|
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), "ARCHIVED")
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -42,8 +42,10 @@ export class IssueDraftService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<any> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, {
|
||||||
|
params: queries,
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response;
|
throw error?.response;
|
||||||
|
@ -53,7 +53,7 @@ export interface IInboxIssue {
|
|||||||
inboxId: string,
|
inboxId: string,
|
||||||
inboxIssueId: string,
|
inboxIssueId: string,
|
||||||
data: Partial<TInboxIssueExtendedDetail>
|
data: Partial<TInboxIssueExtendedDetail>
|
||||||
) => Promise<TInboxIssueExtendedDetail>;
|
) => Promise<void>;
|
||||||
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||||
updateInboxIssueStatus: (
|
updateInboxIssueStatus: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -61,7 +61,7 @@ export interface IInboxIssue {
|
|||||||
inboxId: string,
|
inboxId: string,
|
||||||
inboxIssueId: string,
|
inboxIssueId: string,
|
||||||
data: TInboxDetailedStatus
|
data: TInboxDetailedStatus
|
||||||
) => Promise<TInboxIssueExtendedDetail>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InboxIssue implements IInboxIssue {
|
export class InboxIssue implements IInboxIssue {
|
||||||
@ -215,22 +215,9 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
issue: data,
|
issue: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
this.rootStore.inbox.rootStore.issue.issues.updateIssue(inboxIssueId, data);
|
||||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
|
||||||
this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue);
|
|
||||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
|
||||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
|
||||||
});
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
|
||||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
|
||||||
return uniq(concat(inboxIssueIds, response.id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -238,7 +225,7 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
|
|
||||||
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
pull(this.inboxIssues[inboxId], inboxIssueId);
|
pull(this.inboxIssues[inboxId], inboxIssueId);
|
||||||
@ -248,7 +235,6 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -262,34 +248,18 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
data: TInboxDetailedStatus
|
data: TInboxDetailedStatus
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.inboxIssueService.updateInboxIssueStatus(
|
await this.inboxIssueService.updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
inboxId,
|
|
||||||
inboxIssueId,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
|
|
||||||
const pendingStatus = -2;
|
const pendingStatus = -2;
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
set(this.inboxIssueMap, [inboxId, inboxIssueId, "status"], data.status);
|
||||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
|
||||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
|
||||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
|
||||||
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
|
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
|
||||||
data.status === pendingStatus ? count + 1 : count - 1
|
data.status === pendingStatus ? count + 1 : count - 1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
|
||||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
|
||||||
return uniq(concat(inboxIssueIds, response.id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,9 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
|||||||
|
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
|
||||||
|
this.rootStore.issues.updateIssue(issueId, data);
|
||||||
|
|
||||||
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
|
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { makeObservable } from "mobx";
|
import { makeObservable } from "mobx";
|
||||||
// services
|
// services
|
||||||
import { IssueArchiveService, IssueService } from "services/issue";
|
import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store";
|
|||||||
|
|
||||||
export interface IIssueStoreActions {
|
export interface IIssueStoreActions {
|
||||||
// actions
|
// actions
|
||||||
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
|
fetchIssue: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
issueType?: "DEFAULT" | "DRAFT" | "ARCHIVED"
|
||||||
|
) => Promise<TIssue>;
|
||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
// services
|
// services
|
||||||
issueService;
|
issueService;
|
||||||
issueArchiveService;
|
issueArchiveService;
|
||||||
|
issueDraftService;
|
||||||
|
|
||||||
constructor(rootStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {});
|
makeObservable(this, {});
|
||||||
@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
// services
|
// services
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.issueArchiveService = new IssueArchiveService();
|
this.issueArchiveService = new IssueArchiveService();
|
||||||
|
this.issueDraftService = new IssueDraftService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper methods
|
// helper methods
|
||||||
@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
|
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => {
|
||||||
try {
|
try {
|
||||||
const query = {
|
const query = {
|
||||||
expand: "issue_reactions,issue_attachment,issue_link,parent",
|
expand: "issue_reactions,issue_attachment,issue_link,parent",
|
||||||
};
|
};
|
||||||
|
|
||||||
let issue: TIssue;
|
let issue: TIssue;
|
||||||
|
let issuePayload: TIssue;
|
||||||
|
|
||||||
if (isArchived)
|
if (issueType === "ARCHIVED")
|
||||||
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
|
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
|
||||||
|
else if (issueType === "DRAFT")
|
||||||
|
issue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId, query);
|
||||||
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
||||||
|
|
||||||
if (!issue) throw new Error("Issue not found");
|
if (!issue) throw new Error("Issue not found");
|
||||||
|
|
||||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true);
|
issuePayload = {
|
||||||
|
id: issue?.id,
|
||||||
|
sequence_id: issue?.sequence_id,
|
||||||
|
name: issue?.name,
|
||||||
|
description_html: issue?.description_html,
|
||||||
|
sort_order: issue?.sort_order,
|
||||||
|
state_id: issue?.state_id,
|
||||||
|
priority: issue?.priority,
|
||||||
|
label_ids: issue?.label_ids,
|
||||||
|
assignee_ids: issue?.assignee_ids,
|
||||||
|
estimate_point: issue?.estimate_point,
|
||||||
|
sub_issues_count: issue?.sub_issues_count,
|
||||||
|
attachment_count: issue?.attachment_count,
|
||||||
|
link_count: issue?.link_count,
|
||||||
|
project_id: issue?.project_id,
|
||||||
|
parent_id: issue?.parent_id,
|
||||||
|
cycle_id: issue?.cycle_id,
|
||||||
|
module_ids: issue?.module_ids,
|
||||||
|
created_at: issue?.created_at,
|
||||||
|
updated_at: issue?.updated_at,
|
||||||
|
start_date: issue?.start_date,
|
||||||
|
target_date: issue?.target_date,
|
||||||
|
completed_at: issue?.completed_at,
|
||||||
|
archived_at: issue?.archived_at,
|
||||||
|
created_by: issue?.created_by,
|
||||||
|
updated_by: issue?.updated_by,
|
||||||
|
is_draft: issue?.is_draft,
|
||||||
|
is_subscribed: issue?.is_subscribed,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload], true);
|
||||||
|
|
||||||
// store handlers from issue detail
|
// store handlers from issue detail
|
||||||
// parent
|
// parent
|
||||||
|
@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||||
|
|
||||||
// issue
|
// issue
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) =>
|
fetchIssue = async (
|
||||||
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived);
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
issueType: "DEFAULT" | "ARCHIVED" | "DRAFT" = "DEFAULT"
|
||||||
|
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueType);
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
|
Loading…
Reference in New Issue
Block a user