mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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,
|
||||
)
|
||||
|
||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
if request.data.get(
|
||||
"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()
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
|
@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
id: inboxIssueId,
|
||||
state: "SUCCESS",
|
||||
element: "Inbox page",
|
||||
}
|
||||
},
|
||||
});
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
<DayPicker
|
||||
selected={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"
|
||||
className="border border-custom-border-200 rounded-md p-3"
|
||||
disabled={[{
|
||||
before: tomorrow,
|
||||
}]}
|
||||
disabled={[
|
||||
{
|
||||
before: tomorrow,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
@ -54,7 +54,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
||||
showToast: boolean = true
|
||||
) => {
|
||||
try {
|
||||
const response = await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
||||
await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
||||
if (showToast) {
|
||||
setToastAlert({
|
||||
title: "Issue updated successfully",
|
||||
@ -64,7 +64,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: "Inbox issue updated",
|
||||
payload: { ...response, state: "SUCCESS", element: "Inbox" },
|
||||
payload: { ...data, state: "SUCCESS", element: "Inbox" },
|
||||
updates: {
|
||||
changed_property: Object.keys(data).join(","),
|
||||
change_details: Object.values(data).join(","),
|
||||
|
@ -154,6 +154,10 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
isInboxIssue
|
||||
onLabelUpdate={(val: string[]) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { label_ids: val })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,6 +13,8 @@ export type TIssueLabel = {
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
isInboxIssue?: boolean;
|
||||
onLabelUpdate?: (labelIds: string[]) => void;
|
||||
};
|
||||
|
||||
export type TLabelOperations = {
|
||||
@ -21,7 +23,7 @@ export type TLabelOperations = {
|
||||
};
|
||||
|
||||
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
|
||||
const { updateIssue } = useIssueDetail();
|
||||
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>) => {
|
||||
try {
|
||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
setToastAlert({
|
||||
title: "Issue updated successfully",
|
||||
type: "success",
|
||||
message: "Issue updated successfully",
|
||||
});
|
||||
if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
|
||||
else await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
if (!isInboxIssue)
|
||||
setToastAlert({
|
||||
title: "Issue updated successfully",
|
||||
type: "success",
|
||||
message: "Issue updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Issue update failed",
|
||||
@ -48,11 +52,12 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
||||
try {
|
||||
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
||||
setToastAlert({
|
||||
title: "Label created successfully",
|
||||
type: "success",
|
||||
message: "Label created successfully",
|
||||
});
|
||||
if (!isInboxIssue)
|
||||
setToastAlert({
|
||||
title: "Label created successfully",
|
||||
type: "success",
|
||||
message: "Label created successfully",
|
||||
});
|
||||
return labelResponse;
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
}
|
||||
},
|
||||
}),
|
||||
[updateIssue, createLabel, setToastAlert]
|
||||
[updateIssue, createLabel, setToastAlert, onLabelUpdate]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
);
|
||||
};
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
is_draft: isDraftIssue ? false : issue.is_draft,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
|
@ -60,7 +60,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
<DraftKanBanLayout />
|
||||
) : null}
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview />
|
||||
<IssuePeekOverview is_draft />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
StateDropdown,
|
||||
} from "components/dropdowns";
|
||||
// ui
|
||||
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
|
||||
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.description_html) setValue("description_html", data?.description_html);
|
||||
}, [data?.description_html]);
|
||||
|
||||
const issueName = watch("name");
|
||||
|
||||
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="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
tabIndex={getTabIndex("feeling_lucky")}
|
||||
>
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response"
|
||||
) : (
|
||||
<>
|
||||
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
||||
</>
|
||||
)}
|
||||
</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={
|
||||
{data?.description_html === undefined ? (
|
||||
<Loader className="min-h-[7rem] space-y-2 py-2 border border-custom-border-200 rounded-md p-2 overflow-hidden">
|
||||
<Loader.Item width="100%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<Loader.Item width="80%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="50%" height="26px" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||
<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")}
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-80 ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
tabIndex={getTabIndex("feeling_lucky")}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response"
|
||||
) : (
|
||||
<>
|
||||
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
||||
</>
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
|
@ -3,7 +3,16 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// 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 useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
@ -39,6 +48,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
||||
const [description, setDescription] = useState<string | undefined>(undefined);
|
||||
// store hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const {
|
||||
@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
||||
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT);
|
||||
const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
|
||||
const { fetchIssue } = useIssueDetail();
|
||||
// store mapping based on current store
|
||||
const issueStores = {
|
||||
[EIssuesStoreType.PROJECT]: {
|
||||
@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
// current store details
|
||||
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(() => {
|
||||
// fetching issue details
|
||||
if (isOpen) fetchIssueDetail(data?.id);
|
||||
|
||||
// if modal is closed, reset active project to null
|
||||
// and return to avoid activeProjectId being set to some other project
|
||||
if (!isOpen) {
|
||||
@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
// in the url. This has the least priority.
|
||||
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
|
||||
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
|
||||
|
||||
// clearing up the description state when we leave the component
|
||||
return () => setDescription(undefined);
|
||||
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
|
||||
|
||||
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
|
||||
@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
|
||||
try {
|
||||
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);
|
||||
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;
|
||||
|
||||
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({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
changesMade={changesMade}
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : 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
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : 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 {
|
||||
is_archived?: boolean;
|
||||
is_draft?: boolean;
|
||||
}
|
||||
|
||||
export type TIssuePeekOperations = {
|
||||
@ -45,7 +46,7 @@ export type TIssuePeekOperations = {
|
||||
};
|
||||
|
||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
const { is_archived = false } = props;
|
||||
const { is_archived = false, is_draft = false } = props;
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
// router
|
||||
@ -72,7 +73,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
() => ({
|
||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
|
||||
await fetchIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching the parent issue");
|
||||
}
|
||||
@ -302,6 +308,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
}),
|
||||
[
|
||||
is_archived,
|
||||
is_draft,
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
|
@ -42,7 +42,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
||||
: null,
|
||||
workspaceSlug && projectId && archivedIssueId
|
||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true)
|
||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), "ARCHIVED")
|
||||
: null
|
||||
);
|
||||
|
||||
|
@ -42,8 +42,10 @@ export class IssueDraftService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
|
||||
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
|
@ -53,7 +53,7 @@ export interface IInboxIssue {
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: Partial<TInboxIssueExtendedDetail>
|
||||
) => Promise<TInboxIssueExtendedDetail>;
|
||||
) => Promise<void>;
|
||||
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||
updateInboxIssueStatus: (
|
||||
workspaceSlug: string,
|
||||
@ -61,7 +61,7 @@ export interface IInboxIssue {
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: TInboxDetailedStatus
|
||||
) => Promise<TInboxIssueExtendedDetail>;
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class InboxIssue implements IInboxIssue {
|
||||
@ -215,22 +215,9 @@ export class InboxIssue implements IInboxIssue {
|
||||
issue: data,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
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));
|
||||
});
|
||||
});
|
||||
this.rootStore.inbox.rootStore.issue.issues.updateIssue(inboxIssueId, data);
|
||||
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@ -238,7 +225,7 @@ export class InboxIssue implements IInboxIssue {
|
||||
|
||||
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||
await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.inboxIssues[inboxId], inboxIssueId);
|
||||
@ -248,7 +235,6 @@ export class InboxIssue implements IInboxIssue {
|
||||
});
|
||||
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@ -262,34 +248,18 @@ export class InboxIssue implements IInboxIssue {
|
||||
data: TInboxDetailedStatus
|
||||
) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.updateInboxIssueStatus(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxId,
|
||||
inboxIssueId,
|
||||
data
|
||||
);
|
||||
await this.inboxIssueService.updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
|
||||
|
||||
const pendingStatus = -2;
|
||||
runInAction(() => {
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
||||
set(this.inboxIssueMap, [inboxId, inboxIssueId, "status"], data.status);
|
||||
|
||||
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
|
||||
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);
|
||||
return response as any;
|
||||
} catch (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>) => {
|
||||
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) {
|
||||
runInAction(() => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { makeObservable } from "mobx";
|
||||
// services
|
||||
import { IssueArchiveService, IssueService } from "services/issue";
|
||||
import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { computedFn } from "mobx-utils";
|
||||
@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueStoreActions {
|
||||
// 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>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore {
|
||||
// services
|
||||
issueService;
|
||||
issueArchiveService;
|
||||
issueDraftService;
|
||||
|
||||
constructor(rootStore: IIssueDetail) {
|
||||
makeObservable(this, {});
|
||||
@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore {
|
||||
// services
|
||||
this.issueService = new IssueService();
|
||||
this.issueArchiveService = new IssueArchiveService();
|
||||
this.issueDraftService = new IssueDraftService();
|
||||
}
|
||||
|
||||
// helper methods
|
||||
@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore {
|
||||
});
|
||||
|
||||
// actions
|
||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
|
||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => {
|
||||
try {
|
||||
const query = {
|
||||
expand: "issue_reactions,issue_attachment,issue_link,parent",
|
||||
};
|
||||
|
||||
let issue: TIssue;
|
||||
let issuePayload: TIssue;
|
||||
|
||||
if (isArchived)
|
||||
if (issueType === "ARCHIVED")
|
||||
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);
|
||||
|
||||
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
|
||||
// parent
|
||||
|
@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail {
|
||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||
|
||||
// issue
|
||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) =>
|
||||
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived);
|
||||
fetchIssue = async (
|
||||
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>) =>
|
||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
|
Loading…
Reference in New Issue
Block a user