From da735f318a75a058fa1ea0cef8c2261d1fa5e949 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:17:32 +0530 Subject: [PATCH] [WEB-404] chore: calendar layout add existing issue workflow improvement (#3877) * chore: target date none filter * chore: calendar layout add existing issue functionality added for cycle and module * fix: enums export in the types package * chore: remove NestedKeyOf type --------- Co-authored-by: NarayanBavisetti Co-authored-by: Aaryan Khandelwal --- apiserver/plane/app/views/search.py | 4 + packages/types/src/projects.d.ts | 1 + .../calendar/base-calendar-root.tsx | 12 +- .../issue-layouts/calendar/calendar.tsx | 4 + .../issue-layouts/calendar/day-tile.tsx | 6 +- .../calendar/quick-add-issue-form.tsx | 108 +++++++++++++++--- .../calendar/roots/cycle-root.tsx | 18 ++- .../calendar/roots/module-root.tsx | 22 +++- .../issue-layouts/calendar/week-days.tsx | 3 + web/constants/dashboard.ts | 1 - 10 files changed, 147 insertions(+), 32 deletions(-) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index a2ed1c015..ba8e2e0c3 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -235,6 +235,7 @@ class IssueSearchEndpoint(BaseAPIView): cycle = request.query_params.get("cycle", "false") module = request.query_params.get("module", False) sub_issue = request.query_params.get("sub_issue", "false") + target_date = request.query_params.get("target_date", True) issue_id = request.query_params.get("issue_id", False) @@ -273,6 +274,9 @@ class IssueSearchEndpoint(BaseAPIView): if module: issues = issues.exclude(issue_module__module=module) + if target_date == "none": + issues = issues.filter(target_date__isnull=True) + return Response( issues.values( "name", diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a93734186..a6da364b9 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = { sub_issue?: boolean; issue_id?: string; workspace_search: boolean; + target_date?: string; }; export interface ISearchIssueResponse { diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 2a8cbcc26..ab47a7399 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -29,12 +29,21 @@ interface IBaseCalendarRoot { [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, isCompletedCycle = false } = props; + const { + issueStore, + issuesFilterStore, + QuickActions, + issueActions, + addIssuesToView, + viewId, + isCompletedCycle = false, + } = props; // router const router = useRouter(); @@ -128,6 +137,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { readOnly={!isEditingAllowed || isCompletedCycle} /> )} + addIssuesToView={addIssuesToView} quickAddCallback={issueStore.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 3089a45c4..308393267 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -30,6 +30,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -43,6 +44,7 @@ export const CalendarChart: React.FC = observer((props) => { showWeekends, quickActions, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -90,6 +92,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> @@ -106,6 +109,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 849b967ce..8ac1e460c 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite"; // components import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; // helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; @@ -27,6 +28,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -112,6 +115,7 @@ export const CalendarDayTile: React.FC = observer((props) => { target_date: renderFormattedPayloadDate(date.date) ?? undefined, }} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} onOpen={() => setShowAllIssues(true)} /> diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 5738e028e..5f62706dc 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -2,20 +2,24 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; +// components +import { ExistingIssuesListModal } from "components/core"; // hooks -import { PlusIcon } from "lucide-react"; -import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -import { ISSUE_CREATED } from "constants/event-tracker"; -import { createIssuePayload } from "helpers/issue.helper"; -import { useEventTracker, useProject } from "hooks/store"; +import { useEventTracker, useIssueDetail, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers +import { createIssuePayload } from "helpers/issue.helper"; // icons +import { PlusIcon } from "lucide-react"; // ui +import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; +import { ISearchIssueResponse, TIssue } from "@plane/types"; // constants +import { ISSUE_CREATED } from "constants/event-tracker"; +// helper +import { cn } from "helpers/common.helper"; type Props = { formKey: keyof TIssue; @@ -28,6 +32,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; onOpen?: () => void; }; @@ -60,21 +65,26 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props; + const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); + const { updateIssue } = useIssueDetail(); // refs const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; + const ExistingIssuesListModalPayload = moduleId + ? { module: moduleId.toString(), target_date: "none" } + : { cycle: true, target_date: "none" }; const { reset, @@ -158,13 +168,50 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { } }; - const handleOpen = () => { + const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId) return; + + const issueIds = data.map((i) => i.id); + + try { + // To handle all updates in parallel + await Promise.all( + data.map((issue) => + updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) + ) + ); + if (addIssuesToView) { + await addIssuesToView(issueIds); + } + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + } + }; + + const handleNewIssue = () => { setIsOpen(true); if (onOpen) onOpen(); }; + const handleExistingIssue = () => { + setIsExistingIssueModalOpen(true); + }; return ( <> + {workspaceSlug && projectId && ( + setIsExistingIssueModalOpen(false)} + searchParams={ExistingIssuesListModalPayload} + handleOnSubmit={handleAddIssuesToView} + /> + )} {isOpen && (
= observer((props) => { )} {!isOpen && ( -
- +
+ {addIssuesToView ? ( + setIsMenuOpen(true)} + onMenuClose={() => setIsMenuOpen(false)} + className="w-full" + customButtonClassName="w-full" + customButton={ +
+ + New Issue +
+ } + > + New Issue + Add existing issue +
+ ) : ( + + )}
)} diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 7a30d187e..80a21838d 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,15 +1,16 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; //hooks -import { CycleIssueQuickActions } from "components/issues"; -import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components +import { CycleIssueQuickActions } from "components/issues"; +import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; -import { BaseCalendarRoot } from "../base-calendar-root"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const CycleCalendarLayout: React.FC = observer(() => { const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); @@ -46,11 +47,20 @@ export const CycleCalendarLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); + return ( { const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { + const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; @@ -42,12 +43,21 @@ export const ModuleCalendarLayout: React.FC = observer(() => { [issues, workspaceSlug, moduleId] ); + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }, + [issues?.addIssuesToModule, workspaceSlug, projectId, moduleId] + ); + return ( ); diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 2ce742fe8..ec1d12e59 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -25,6 +25,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -68,6 +70,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3d11b4f12..3d99a4679 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -11,7 +11,6 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; - // constants import { EUserWorkspaceRoles } from "./workspace"; // icons