From 6a16a98b03d1f656ffa53c6ee0592b5aa1cbce0c Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 16 Jan 2024 12:46:03 +0530 Subject: [PATCH] chore: update in sub-issues component and property validation and issue loaders (#3375) * fix: handled undefined issue_id in list layout * chore: refactor peek overview and user role validation. * chore: sub issues * fix: sub issues state distribution changed * chore: sub_issues implementation in issue detail page * chore: fixes in cycle/ module layout. * Fix progress chart * Module issues's update/ delete. * Peek Overview for Modules/ Cycle. * Fix Cycle Filters not applying bug. --------- Co-authored-by: Prateek Shourya Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/issue.py | 38 +- .../types/src/issues/issue_sub_issues.d.ts | 10 +- .../core/sidebar/progress-chart.tsx | 2 +- .../core/sidebar/sidebar-progress-stats.tsx | 2 +- web/components/issues/description-form.tsx | 14 +- .../issue-detail/label/label-list-item.tsx | 2 +- .../issues/issue-detail/links/root.tsx | 2 +- .../issues/issue-detail/main-content.tsx | 5 +- .../issues/issue-detail/parent/siblings.tsx | 4 +- web/components/issues/issue-detail/root.tsx | 47 +- .../issues/issue-detail/sidebar.tsx | 8 +- .../issue-layouts/list/roots/module-root.tsx | 4 +- .../issue-layouts/roots/cycle-layout-root.tsx | 31 +- .../roots/module-layout-root.tsx | 32 +- .../roots/project-layout-root.tsx | 6 +- web/components/issues/issue-modal/form.tsx | 29 +- web/components/issues/issue-modal/modal.tsx | 80 ++-- .../issues/peek-overview/issue-detail.tsx | 225 ++-------- .../issues/peek-overview/properties.tsx | 152 +++---- web/components/issues/peek-overview/root.tsx | 177 ++++---- web/components/issues/peek-overview/view.tsx | 210 ++++----- .../issues/sub-issues/issue-list-item.tsx | 199 +++++++++ web/components/issues/sub-issues/issue.tsx | 192 --------- .../issues/sub-issues/issues-list.tsx | 128 +++--- .../issues/sub-issues/properties.tsx | 101 ++--- web/components/issues/sub-issues/root.tsx | 404 +++++++++++------- web/components/modules/sidebar.tsx | 2 +- web/store/issue/cycle/filter.store.ts | 2 +- web/store/issue/issue-details/issue.store.ts | 4 + web/store/issue/issue-details/root.store.ts | 17 +- .../issue/issue-details/sub_issues.store.ts | 163 ++++--- web/store/issue/project/issue.store.ts | 18 +- 32 files changed, 1136 insertions(+), 1174 deletions(-) create mode 100644 web/components/issues/sub-issues/issue-list-item.tsx delete mode 100644 web/components/issues/sub-issues/issue.tsx diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 837aa7e7b..967147aeb 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -82,7 +82,7 @@ from plane.db.models import ( from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters - +from collections import defaultdict class IssueViewSet(WebhookMixin, BaseViewSet): def get_serializer_class(self): @@ -784,22 +784,13 @@ class SubIssuesEndpoint(BaseAPIView): queryset=IssueReaction.objects.select_related("actor"), ) ) + .annotate(state_group=F("state__group")) ) - state_distribution = ( - State.objects.filter( - workspace__slug=slug, state_issue__parent_id=issue_id - ) - .annotate(state_group=F("group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - result = { - item["state_group"]: item["state_count"] - for item in state_distribution - } + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) serializer = IssueSerializer( sub_issues, @@ -831,7 +822,7 @@ class SubIssuesEndpoint(BaseAPIView): _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group")) # Track the issue _ = [ @@ -846,11 +837,24 @@ class SubIssuesEndpoint(BaseAPIView): ) for sub_issue_id in sub_issue_ids ] + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in updated_sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + serializer = IssueSerializer( + updated_sub_issues, + many=True, + ) return Response( - IssueSerializer(updated_sub_issues, many=True).data, + { + "sub_issues": serializer.data, + "state_distribution": result, + }, status=status.HTTP_200_OK, ) + class IssueLinkViewSet(BaseViewSet): diff --git a/packages/types/src/issues/issue_sub_issues.d.ts b/packages/types/src/issues/issue_sub_issues.d.ts index 76dcf1288..e604761ed 100644 --- a/packages/types/src/issues/issue_sub_issues.d.ts +++ b/packages/types/src/issues/issue_sub_issues.d.ts @@ -1,11 +1,11 @@ import { TIssue } from "./issue"; export type TSubIssuesStateDistribution = { - backlog: number; - unstarted: number; - started: number; - completed: number; - cancelled: number; + backlog: string[]; + unstarted: string[]; + started: string[]; + completed: string[]; + cancelled: string[]; }; export type TIssueSubIssues = { diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index 274e5da0c..3d47d8eca 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -41,7 +41,7 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) => )); const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues }) => { - const chartData = Object.keys(distribution).map((key) => ({ + const chartData = Object.keys(distribution ?? []).map((key) => ({ currentDate: renderFormattedDateWithoutYear(key), pending: distribution[key], })); diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 698695ee4..c37cdf4b9 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -183,7 +183,7 @@ export const SidebarProgressStats: React.FC = ({ )} - {distribution.labels.length > 0 ? ( + {distribution?.labels.length > 0 ? ( distribution.labels.map((label, index) => ( = (props) => { async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - await issueOperations.update(workspaceSlug, projectId, issueId, { - name: formData.name ?? "", - description_html: formData.description_html ?? "

", - }); + await issueOperations.update( + workspaceSlug, + projectId, + issueId, + { + name: formData.name ?? "", + description_html: formData.description_html ?? "

", + }, + false + ); }, [workspaceSlug, projectId, issueId, issueOperations] ); diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx index 926d287aa..3c3164c5a 100644 --- a/web/components/issues/issue-detail/label/label-list-item.tsx +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -25,7 +25,7 @@ export const LabelListItem: FC = (props) => { const label = getLabelById(labelId); const handleLabel = async () => { - if (issue) { + if (issue && !disabled) { const currentLabels = issue.label_ids.filter((_labelId) => _labelId !== labelId); await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); } diff --git a/web/components/issues/issue-detail/links/root.tsx b/web/components/issues/issue-detail/links/root.tsx index 1c226b7a7..94124085a 100644 --- a/web/components/issues/issue-detail/links/root.tsx +++ b/web/components/issues/issue-detail/links/root.tsx @@ -107,7 +107,7 @@ export const IssueLinkRoot: FC = (props) => { linkOperations={handleLinkOperations} /> -
+

Links

{!disabled && ( diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 6e7ac4289..fcbe54a1c 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -87,10 +87,9 @@ export const IssueMainContent: React.FC = observer((props) => { )}
diff --git a/web/components/issues/issue-detail/parent/siblings.tsx b/web/components/issues/issue-detail/parent/siblings.tsx index b8ebc9ec9..bc93ff138 100644 --- a/web/components/issues/issue-detail/parent/siblings.tsx +++ b/web/components/issues/issue-detail/parent/siblings.tsx @@ -23,10 +23,10 @@ export const IssueParentSiblings: FC = (props) => { } = useIssueDetail(); const { isLoading } = useSWR( - peekIssue && parentIssue + peekIssue && parentIssue && parentIssue.project_id ? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}` : null, - peekIssue && parentIssue + peekIssue && parentIssue && parentIssue.project_id ? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id) : null ); diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index b52857e0a..4243ba03e 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -1,6 +1,7 @@ import { FC, useMemo } from "react"; import { useRouter } from "next/router"; // components +import { IssuePeekOverview } from "components/issues"; import { IssueMainContent } from "./main-content"; import { IssueDetailsSidebar } from "./sidebar"; // ui @@ -8,16 +9,23 @@ import { EmptyState } from "components/common"; // images import emptyIssue from "public/empty-state/issue.svg"; // hooks -import { useIssueDetail, useUser } from "hooks/store"; +import { useIssueDetail, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + update: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + showToast?: boolean + ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; @@ -47,6 +55,9 @@ export const IssueDetailRoot: FC = (props) => { addIssueToModule, removeIssueFromModule, } = useIssueDetail(); + const { + issues: { removeIssue: removeArchivedIssue }, + } = useIssues(EIssuesStoreType.ARCHIVED); const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, @@ -61,14 +72,22 @@ export const IssueDetailRoot: FC = (props) => { console.error("Error fetching the parent issue"); } }, - update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + update: async ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + showToast: boolean = true + ) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); + if (showToast) { + setToastAlert({ + title: "Issue updated successfully", + type: "success", + message: "Issue updated successfully", + }); + } } catch (error) { setToastAlert({ title: "Issue update failed", @@ -79,7 +98,8 @@ export const IssueDetailRoot: FC = (props) => { }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - await removeIssue(workspaceSlug, projectId, issueId); + if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId); + else await removeIssue(workspaceSlug, projectId, issueId); setToastAlert({ title: "Issue deleted successfully", type: "success", @@ -159,9 +179,11 @@ export const IssueDetailRoot: FC = (props) => { }, }), [ + is_archived, fetchIssue, updateIssue, removeIssue, + removeArchivedIssue, addIssueToCycle, removeIssueFromCycle, addIssueToModule, @@ -170,9 +192,9 @@ export const IssueDetailRoot: FC = (props) => { ] ); - // Issue details + // issue details const issue = getIssueById(issueId); - // Check if issue is editable, based on user role + // checking if issue is editable, based on user role const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( @@ -211,6 +233,9 @@ export const IssueDetailRoot: FC = (props) => {
)} + + {/* peek overview */} + ); }; diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index a80f88730..6b249f4bd 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -162,7 +162,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { /> )} - {/* {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( - )} */} + )} - {/* {isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( + {is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( - )} */} + )} diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 947cfe55b..7dbe38090 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -25,12 +25,12 @@ export const ModuleListLayout: React.FC = observer(() => { [EIssueActions.UPDATE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); }, [EIssueActions.DELETE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); }, [EIssueActions.REMOVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index e5398ef90..be518ea04 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -13,6 +13,7 @@ import { CycleKanBanLayout, CycleListLayout, CycleSpreadsheetLayout, + IssuePeekOverview, } from "components/issues"; import { TransferIssues, TransferIssuesModal } from "components/cycles"; // ui @@ -73,19 +74,23 @@ export const CycleLayoutRoot: React.FC = observer(() => { cycleId={cycleId.toString()} /> ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ <> +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ {/* peek overview */} + + )} )} diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 4478a0faa..808cad91b 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -6,6 +6,7 @@ import useSWR from "swr"; import { useIssues } from "hooks/store"; // components import { + IssuePeekOverview, ModuleAppliedFiltersRoot, ModuleCalendarLayout, ModuleEmptyState, @@ -16,6 +17,7 @@ import { } from "components/issues"; // ui import { Spinner } from "@plane/ui"; +// constants import { EIssuesStoreType } from "constants/issue"; export const ModuleLayoutRoot: React.FC = observer(() => { @@ -62,19 +64,23 @@ export const ModuleLayoutRoot: React.FC = observer(() => { moduleId={moduleId.toString()} /> ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ <> +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ {/* peek overview */} + + )} )} diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index bfff19cd8..1edba5563 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -30,7 +30,11 @@ export const ProjectLayoutRoot: FC = observer(() => { useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, async () => { if (workspaceSlug && projectId) { await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); - await issues?.fetchIssues(workspaceSlug.toString(), projectId.toString(), issues?.groupedIssueIds ? "mutation" : "init-loader"); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader" + ); } }); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 1853f9546..d7c543079 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState, useRef } from "react"; +import React, { FC, useState, useRef, useEffect } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; @@ -6,7 +6,7 @@ import { LayoutPanelTop, Sparkle, X } from "lucide-react"; // editor import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks -import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; +import { useApplication, useEstimate, useIssueDetail, useMention, useProject } from "hooks/store"; import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; @@ -85,6 +85,9 @@ export const IssueFormRoot: FC = observer((props) => { const { getProjectById } = useProject(); const { areEstimatesEnabledForProject } = useEstimate(); const { mentionHighlights, mentionSuggestions } = useMention(); + const { + issue: { getIssueById }, + } = useIssueDetail(); // toast alert const { setToastAlert } = useToast(); // form info @@ -179,6 +182,28 @@ export const IssueFormRoot: FC = observer((props) => { const projectDetails = getProjectById(projectId); + // executing this useEffect when the parent_id coming from the component prop + useEffect(() => { + const parentId = watch("parent_id") || undefined; + if (!parentId) return; + if (parentId === selectedParentIssue?.id || selectedParentIssue) return; + + const issue = getIssueById(parentId); + if (!issue) return; + + const projectDetails = getProjectById(issue.project_id); + if (!projectDetails) return; + + setSelectedParentIssue({ + id: issue.id, + name: issue.name, + project_id: issue.project_id, + project__identifier: projectDetails.identifier, + project__name: projectDetails.name, + sequence_id: issue.sequence_id, + } as ISearchIssueResponse); + }, [watch, getIssueById, getProjectById, selectedParentIssue]); + return ( <> {projectId && ( diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 55a25b33d..81a07d761 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -18,7 +18,7 @@ export interface IssuesModalProps { data?: Partial; isOpen: boolean; onClose: () => void; - onSubmit?: (res: Partial) => Promise; + onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; } @@ -58,52 +58,46 @@ export const CreateUpdateIssueModal: React.FC = observer((prop onClose(); }; - const handleCreateIssue = async (payload: Partial): Promise => { - if (!workspaceSlug || !payload.project_id) return null; + const handleCreateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !payload.project_id) return undefined; - await createIssue(workspaceSlug.toString(), payload.project_id, payload) - .then(async (res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - !createMore && handleClose(); - return res; - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", - }); + try { + const response = await createIssue(workspaceSlug.toString(), payload.project_id, payload); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", }); - - return null; + !createMore && handleClose(); + return response; + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + } }; - const handleUpdateIssue = async (payload: Partial): Promise => { - if (!workspaceSlug || !payload.project_id || !data?.id) return null; + const handleUpdateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !payload.project_id || !data?.id) return undefined; - await updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue updated successfully.", - }); - handleClose(); - return { ...payload, ...res }; - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be updated. Please try again.", - }); + try { + const response = await updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue updated successfully.", }); - - return null; + handleClose(); + return response; + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + } }; const handleFormSubmit = async (formData: Partial) => { @@ -114,7 +108,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop description_html: formData.description_html ?? "

", }; - let res: TIssue | null = null; + let res: TIssue | undefined = undefined; if (!data?.id) res = await handleCreateIssue(payload); else res = await handleUpdateIssue(payload); @@ -126,7 +120,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (formData.module_id && res && (!data?.id || formData.module_id !== data?.module_id)) await addIssueToModule(workspaceSlug.toString(), formData.project_id, formData.module_id, [res.id]); - if (res && onSubmit) await onSubmit(res); + if (res != undefined && onSubmit) await onSubmit(res); }; const handleFormChange = (formData: Partial | null) => setChangesMade(formData); diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 3eb7037f2..8a0ab0fe7 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,133 +1,32 @@ -import { ChangeEvent, FC, useCallback, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import debounce from "lodash/debounce"; -// packages -import { RichTextEditor } from "@plane/rich-text-editor"; +import { FC } from "react"; // hooks -import { useMention, useProject, useUser } from "hooks/store"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; +import { useIssueDetail, useProject, useUser } from "hooks/store"; // components -import { IssuePeekOverviewReactions } from "components/issues"; -// ui -import { TextArea } from "@plane/ui"; -// types -import { TIssue, IUser } from "@plane/types"; -// services -import { FileService } from "services/file.service"; -// constants -import { EUserProjectRoles } from "constants/project"; - -const fileService = new FileService(); +import { IssueDescriptionForm, TIssueOperations } from "components/issues"; +import { IssueReaction } from "../issue-detail/reactions"; interface IPeekOverviewIssueDetails { workspaceSlug: string; - issue: TIssue; - issueReactions: any; - user: IUser | null; - issueUpdate: (issue: Partial) => void; - issueReactionCreate: (reaction: string) => void; - issueReactionRemove: (reaction: string) => void; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + is_archived: boolean; + disabled: boolean; isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } export const PeekOverviewIssueDetails: FC = (props) => { - const { - workspaceSlug, - issue, - issueReactions, - user, - issueUpdate, - issueReactionCreate, - issueReactionRemove, - isSubmitting, - setIsSubmitting, - } = props; - // states - const [characterLimit, setCharacterLimit] = useState(false); + const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); - const { mentionHighlights, mentionSuggestions } = useMention(); const { getProjectById } = useProject(); - // derived values - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - // toast alert - const { setShowAlert } = useReloadConfirmations(); - // form info + const { currentUser } = useUser(); const { - handleSubmit, - watch, - reset, - control, - formState: { errors }, - } = useForm({ - defaultValues: { - name: issue.name, - description_html: issue.description_html, - }, - }); - - const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { - if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - - await issueUpdate({ - ...issue, - name: formData.name ?? "", - description_html: formData.description_html ?? "

", - }); - }, - [issue, issueUpdate] - ); - - const [localTitleValue, setLocalTitleValue] = useState(""); - const [localIssueDescription, setLocalIssueDescription] = useState({ - id: issue.id, - description_html: issue.description_html, - }); - - // adding issue.description_html or issue.name to dependency array causes - // editor rerendering on every save - useEffect(() => { - if (issue.id) { - setLocalIssueDescription({ id: issue.id, description_html: issue.description_html }); - setLocalTitleValue(issue.name); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [issue.id]); // TODO: Verify the exhaustive-deps warning - - // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS - // TODO: Verify the exhaustive-deps warning - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedFormSave = useCallback( - debounce(async () => { - handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); - }, 1500), - [handleSubmit] - ); - - useEffect(() => { - if (isSubmitting === "submitted") { - setShowAlert(false); - setTimeout(async () => { - setIsSubmitting("saved"); - }, 2000); - } else if (isSubmitting === "submitting") { - setShowAlert(true); - } - }, [isSubmitting, setShowAlert, setIsSubmitting]); - - // reset form values - useEffect(() => { - if (!issue) return; - - reset({ - ...issue, - }); - }, [issue, reset]); - + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issue = getIssueById(issueId); + if (!issue) return <>; const projectDetails = getProjectById(issue?.project_id); return ( @@ -135,82 +34,24 @@ export const PeekOverviewIssueDetails: FC = (props) = {projectDetails?.identifier}-{issue?.sequence_id} - -
- {isAllowed ? ( - ( -