From d78b4dccf35111184e7312f7dc6fe85acd32f7cf Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:07:46 +0530 Subject: [PATCH] chore: implemented CRUD operations in all the layouts (#2505) * chore: basic crud operations added to the list view * refactor: cycle details page * refactor: module details page * chore: added quick actions to kanban issue block * chore: implement quick actions in calendar layout * fix: custom menu component * chore: separate quick action dropdowns implemented * style: loader for calendar * fix: build errors --- .../tailwind-config-custom/tailwind.config.js | 4 + packages/ui/src/dropdowns/custom-menu.tsx | 6 +- .../command-palette/command-pallette.tsx | 1 - web/components/cycles/sidebar.tsx | 148 ++++++++------ web/components/cycles/transfer-issues.tsx | 7 +- web/components/headers/cycle-issues.tsx | 144 ++++++++----- web/components/headers/module-issues.tsx | 147 ++++++++----- web/components/issues/delete-issue-modal.tsx | 116 ++--------- .../issue-layouts/calendar/calendar.tsx | 11 +- .../issue-layouts/calendar/day-tile.tsx | 11 +- .../calendar/dropdowns/months-dropdown.tsx | 43 +++- .../calendar/dropdowns/options-dropdown.tsx | 102 +++++---- .../issue-layouts/calendar/issue-blocks.tsx | 18 +- .../calendar/roots/cycle-root.tsx | 42 +++- .../calendar/roots/module-root.tsx | 48 ++++- .../calendar/roots/project-root.tsx | 34 ++- .../calendar/roots/project-view-root.tsx | 38 +++- .../issue-layouts/calendar/week-days.tsx | 8 +- .../issue-layouts/calendar/week-header.tsx | 16 +- .../display-filters-selection.tsx | 4 +- .../header/display-filters/group-by.tsx | 4 +- .../header/display-filters/issue-type.tsx | 4 +- .../header/display-filters/order-by.tsx | 4 +- web/components/issues/issue-layouts/index.ts | 1 + .../issues/issue-layouts/kanban/block.tsx | 120 +++++------ .../issue-layouts/kanban/blocks-list.tsx | 50 +++++ .../issue-layouts/kanban/cycle-root.tsx | 65 ++++-- .../issues/issue-layouts/kanban/default.tsx | 166 ++++++++------- .../issues/issue-layouts/kanban/index.ts | 2 + .../issue-layouts/kanban/module-root.tsx | 65 ++++-- .../kanban/profile-issues-root.tsx | 53 +++-- .../issues/issue-layouts/kanban/root.tsx | 50 ++++- .../issues/issue-layouts/kanban/swimlanes.tsx | 37 +++- .../issues/issue-layouts/kanban/view-root.tsx | 84 ++++---- .../issues/issue-layouts/list/block.tsx | 85 ++++---- .../issues/issue-layouts/list/blocks-list.tsx | 43 ++++ .../issues/issue-layouts/list/cycle-root.tsx | 51 ++++- .../issues/issue-layouts/list/default.tsx | 22 +- .../issues/issue-layouts/list/index.ts | 2 + .../issues/issue-layouts/list/module-root.tsx | 51 ++++- .../list/profile-issues-root.tsx | 45 +++- .../issues/issue-layouts/list/properties.tsx | 4 +- .../issues/issue-layouts/list/root.tsx | 46 ++++- .../issues/issue-layouts/list/view-root.tsx | 36 ++-- .../quick-action-dropdowns/cycle-issue.tsx | 130 ++++++++++++ .../quick-action-dropdowns/index.ts | 3 + .../quick-action-dropdowns/module-issue.tsx | 130 ++++++++++++ .../quick-action-dropdowns/project-issue.tsx | 117 +++++++++++ .../columns/issue/issue-column.tsx | 32 +-- .../issue/spreadsheet-issue-column.tsx | 6 +- .../spreadsheet/roots/cycle-root.tsx | 8 +- .../spreadsheet/roots/module-root.tsx | 8 +- .../spreadsheet/roots/project-root.tsx | 2 +- .../spreadsheet/roots/project-view-root.tsx | 8 +- .../issues/issue-peek-overview/root.tsx | 2 +- web/components/issues/modal.tsx | 28 +-- .../issues/my-issues/my-issues-view.tsx | 22 +- .../issues/peek-overview/layout.tsx | 27 ++- web/components/issues/sidebar.tsx | 9 +- web/components/issues/sub-issues/root.tsx | 25 +-- web/components/modules/sidebar.tsx | 122 ++++++----- .../profile/profile-issues-view.tsx | 19 +- web/helpers/string.helper.ts | 13 ++ .../projects/[projectId]/cycles/[cycleId].tsx | 193 +++++------------- .../[projectId]/modules/[moduleId].tsx | 156 ++++---------- web/store/cycle/cycle_issue.store.ts | 106 ++++++++-- .../cycle/cycle_issue_kanban_view.store.ts | 7 +- web/store/issue/issue.store.ts | 38 ++++ web/store/issue/issue_detail.store.ts | 40 ++-- web/store/issue/issue_filters.store.ts | 2 +- web/store/issue/issue_kanban_view.store.ts | 7 +- web/store/module/module_issue.store.ts | 87 +++++++- .../module/module_issue_kanban_view.store.ts | 7 +- web/store/module/modules.store.ts | 8 +- web/store/profile-issues/issue.store.ts | 51 +++++ .../project-view/project_view_issues.store.ts | 38 ++++ 76 files changed, 2336 insertions(+), 1153 deletions(-) create mode 100644 web/components/issues/issue-layouts/kanban/blocks-list.tsx create mode 100644 web/components/issues/issue-layouts/list/blocks-list.tsx create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/index.ts create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx create mode 100644 web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 0b8fbe7a8..3825394d3 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -323,6 +323,10 @@ module.exports = { "0%": { right: "-20rem" }, "100%": { right: "0" }, }, + "bar-loader": { + from: { left: "-100%" }, + to: { left: "100%" }, + }, }, typography: ({ theme }) => ({ brand: { diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 729c29bab..0e8d50064 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -35,7 +35,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { React.useState(null); const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", + placement: placement ?? "auto", }); return ( @@ -100,9 +100,9 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { )} )} - +
{ handleClose={() => toggleDeleteIssueModal(false)} isOpen={isDeleteIssueModalOpen} data={issueDetails} - user={user} /> )} diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 5cc761a12..4d8653ac7 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { useForm } from "react-hook-form"; -// headless ui import { Disclosure, Popover, Transition } from "@headlessui/react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { CycleService } from "services/cycle.service"; // hooks @@ -28,32 +30,39 @@ import { AlertCircle, } from "lucide-react"; // helpers -import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; -import { isDateGreaterThanToday, renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; +import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper"; +import { + getDateRangeStatus, + isDateGreaterThanToday, + renderDateFormat, + renderShortDateWithYearFormat, +} from "helpers/date-time.helper"; // types -import { IUser, ICycle } from "types"; +import { ICycle } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; type Props = { - cycle: ICycle | undefined; isOpen: boolean; - cycleStatus: string; - isCompleted: boolean; - user: IUser | undefined; + cycleId: string; }; +// services const cycleService = new CycleService(); -export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatus, isCompleted, user }) => { +// TODO: refactor the whole component +export const CycleDetailsSidebar: React.FC = observer((props) => { + const { isOpen, cycleId } = props; + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId } = router.query; + + const { user: userStore, cycle: cycleDetailsStore } = useMobxStore(); + + const user = userStore.currentUser ?? undefined; + const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; const { setToastAlert } = useToast(); @@ -78,9 +87,7 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu }; const handleCopyText = () => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`) + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`) .then(() => { setToastAlert({ type: "success", @@ -96,11 +103,11 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu }; useEffect(() => { - if (cycle) + if (cycleDetails) reset({ - ...cycle, + ...cycleDetails, }); - }, [cycle, reset]); + }, [cycleDetails, reset]); const dateChecker = async (payload: any) => { try { @@ -129,11 +136,11 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu return; } - if (cycle?.start_date && cycle?.end_date) { + if (cycleDetails?.start_date && cycleDetails?.end_date) { const isDateValidForExistingCycle = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, - cycle_id: cycle.id, + cycle_id: cycleDetails.id, }); if (isDateValidForExistingCycle) { @@ -203,11 +210,11 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu return; } - if (cycle?.start_date && cycle?.end_date) { + if (cycleDetails?.start_date && cycleDetails?.end_date) { const isDateValidForExistingCycle = await dateChecker({ start_date: `${watch("start_date")}`, end_date: `${watch("end_date")}`, - cycle_id: cycle.id, + cycle_id: cycleDetails.id, }); if (isDateValidForExistingCycle) { @@ -258,29 +265,39 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu } }; - const isStartValid = new Date(`${cycle?.start_date}`) <= new Date(); - const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`); + const cycleStatus = + cycleDetails?.start_date && cycleDetails?.end_date + ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) + : "draft"; + const isCompleted = cycleStatus === "completed"; - const progressPercentage = cycle ? Math.round((cycle.completed_issues / cycle.total_issues) * 100) : null; + const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date(); + const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`); + + const progressPercentage = cycleDetails + ? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100) + : null; + + if (!cycleDetails) return null; return ( <> - {cycle && ( + {cycleDetails && workspaceSlug && projectId && ( setCycleDeleteModal(false)} onSubmit={() => {}} - workspaceSlug={workspaceSlug} - projectId={projectId} + workspaceSlug={workspaceSlug.toString()} + projectId={projectId.toString()} /> )}
- {cycle ? ( + {cycleDetails ? ( <>
@@ -296,13 +313,13 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu {renderShortDateWithYearFormat( - new Date(`${watch("start_date") ? watch("start_date") : cycle?.start_date}`), + new Date(`${watch("start_date") ? watch("start_date") : cycleDetails?.start_date}`), "Start date" )} @@ -319,7 +336,7 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu > { if (val) { handleStartDateChange(val); @@ -344,14 +361,14 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu {renderShortDateWithYearFormat( - new Date(`${watch("end_date") ? watch("end_date") : cycle?.end_date}`), + new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`), "End date" )} @@ -368,7 +385,7 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu > { if (val) { handleEndDateChange(val); @@ -391,7 +408,9 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu
-

{cycle.name}

+

+ {cycleDetails.name} +

{!isCompleted && ( @@ -412,7 +431,7 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu
- {cycle.description} + {cycleDetails.description}
@@ -424,20 +443,20 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu
- {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? ( {cycle.owned_by.display_name} ) : ( - {cycle.owned_by.display_name.charAt(0)} + {cycleDetails.owned_by.display_name.charAt(0)} )} - {cycle.owned_by.display_name} + {cycleDetails.owned_by.display_name}
@@ -449,9 +468,9 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu
- + - {cycle.completed_issues}/{cycle.total_issues} + {cycleDetails.completed_issues}/{cycleDetails.total_issues}
@@ -498,7 +517,8 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu Pending Issues -{" "} - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} + {cycleDetails.total_issues - + (cycleDetails.completed_issues + cycleDetails.cancelled_issues)} @@ -515,10 +535,10 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu
@@ -540,7 +560,7 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu Other Information - {cycle.total_issues > 0 ? ( + {cycleDetails.total_issues > 0 ? ( @@ -555,18 +575,18 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu - {cycle.total_issues > 0 ? ( + {cycleDetails.total_issues > 0 ? (
) : ( @@ -595,4 +615,4 @@ export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatu ); -}; +}); diff --git a/web/components/cycles/transfer-issues.tsx b/web/components/cycles/transfer-issues.tsx index a4291f4c0..5ec23cd70 100644 --- a/web/components/cycles/transfer-issues.tsx +++ b/web/components/cycles/transfer-issues.tsx @@ -19,7 +19,9 @@ type Props = { const cycleService = new CycleService(); -export const TransferIssues: React.FC = ({ handleClick }) => { +export const TransferIssues: React.FC = (props) => { + const { handleClick } = props; + const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -33,8 +35,9 @@ export const TransferIssues: React.FC = ({ handleClick }) => { const transferableIssuesCount = cycleDetails ? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues : 0; + return ( -
+
Completed cycles are not editable. diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index f8fbfca40..ac6ed2e6b 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,16 +1,20 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/router"; +import Link from "next/link"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { ProjectAnalyticsModal } from "components/analytics"; // ui -import { Button } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu } from "@plane/ui"; // icons -import { Plus } from "lucide-react"; +import { ArrowRight, ContrastIcon, Plus } from "lucide-react"; +// helpers +import { truncateText } from "helpers/string.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants @@ -28,9 +32,15 @@ export const CycleIssuesHeader: React.FC = observer(() => { cycleIssueFilter: cycleIssueFilterStore, project: projectStore, } = useMobxStore(); - const activeLayout = issueFilterStore.userDisplayFilters.layout; + const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); + + const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; + const toggleSidebar = () => { + setValue(`${!isSidebarCollapsed}`); + }; + const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; @@ -88,6 +98,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); + const cyclesList = projectId ? cycleStore.cycles[projectId.toString()] : undefined; const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; return ( @@ -97,50 +108,91 @@ export const CycleIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} cycleDetails={cycleDetails ?? undefined} /> -
- handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> - - +
+ router.back()}> + + +

{`${truncateText(cycleDetails?.project_detail.name ?? "", 32)} Cycles`}

+
+ + } + /> +
+ + + {cycleDetails?.name && truncateText(cycleDetails.name, 40)} + } - labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + className="ml-1.5 flex-shrink-0" + width="auto" + > + {cyclesList?.map((cycle) => ( + router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} + > + {truncateText(cycle.name, 40)} + + ))} + +
+
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} /> - - - - - - + + m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + /> + + + + + + + +
); diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index c63c93bc4..08a30531c 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -1,16 +1,20 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/router"; +import Link from "next/link"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { ProjectAnalyticsModal } from "components/analytics"; // ui -import { Button } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu } from "@plane/ui"; // icons -import { Plus } from "lucide-react"; +import { ArrowRight, ContrastIcon, Plus } from "lucide-react"; +// helpers +import { truncateText } from "helpers/string.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants @@ -28,9 +32,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => { moduleFilter: moduleFilterStore, project: projectStore, } = useMobxStore(); - const activeLayout = issueFilterStore.userDisplayFilters.layout; + const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); + + const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; + const toggleSidebar = () => { + setValue(`${!isSidebarCollapsed}`); + }; + const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; @@ -88,6 +98,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); + const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined; const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; return ( @@ -97,50 +108,94 @@ export const ModuleIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} /> -
- handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> - - +
+ router.back()}> + + +

{`${truncateText( + moduleDetails?.project_detail.name ?? "", + 32 + )} Modules`}

+
+ + } + /> +
+ + + {moduleDetails?.name && truncateText(moduleDetails.name, 40)} + } - labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + className="ml-1.5 flex-shrink-0" + width="auto" + > + {modulesList?.map((module) => ( + router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)} + > + {truncateText(module.name, 40)} + + ))} + +
+
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} /> - - - - - - + + m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + /> + + + + + + + +
); diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 0ca58f62d..41133111e 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -1,56 +1,31 @@ import { useEffect, useState, Fragment } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// services -import { IssueService, IssueArchiveService } from "services/issue"; -// hooks -import useIssuesView from "hooks/use-issues-view"; -import useToast from "hooks/use-toast"; -// icons import { AlertTriangle } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { Button } from "@plane/ui"; // types -import type { IIssue, IUser, ISubIssueResponse } from "types"; -// fetch-keys -import { - CYCLE_ISSUES_WITH_PARAMS, - MODULE_ISSUES_WITH_PARAMS, - PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS, - PROJECT_ISSUES_LIST_WITH_PARAMS, - SUB_ISSUES, -} from "constants/fetch-keys"; +import type { IIssue } from "types"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue | null; - user: IUser | undefined; + data: IIssue; onSubmit?: () => Promise; - redirection?: boolean; }; -const issueService = new IssueService(); -const issueArchiveService = new IssueArchiveService(); - -export const DeleteIssueModal: React.FC = ({ - isOpen, - handleClose, - data, - user, - onSubmit, - redirection = true, -}) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); +export const DeleteIssueModal: React.FC = observer((props) => { + const { data, isOpen, handleClose, onSubmit } = props; const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, issueId } = router.query; - const isArchivedIssues = router.pathname.includes("archived-issues"); + const { workspaceSlug } = router.query; - const { displayFilters, params } = useIssuesView(); + const { issueDetail: issueDetailStore } = useMobxStore(); - const { setToastAlert } = useToast(); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); useEffect(() => { setIsDeleteLoading(false); @@ -61,77 +36,16 @@ export const DeleteIssueModal: React.FC = ({ handleClose(); }; - const handleDeletion = async () => { - if (!workspaceSlug || !data) return; + const handleIssueDelete = async () => { + if (!workspaceSlug) return; setIsDeleteLoading(true); - await issueService - .deleteIssue(workspaceSlug as string, data.project, data.id, user) - .then(() => { - if (displayFilters.layout === "spreadsheet") { - if (data.parent) { - mutate( - SUB_ISSUES(data.parent.toString()), - (prevData) => { - if (!prevData) return prevData; - const updatedArray = (prevData.sub_issues ?? []).filter((i) => i.id !== data.id); + await issueDetailStore.deleteIssue(workspaceSlug.toString(), data.project, data.id); - return { - ...prevData, - sub_issues: updatedArray, - }; - }, - false - ); - } - } else { - if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); - else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); - else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params)); - } - - if (onSubmit) onSubmit(); - - handleClose(); - setToastAlert({ - title: "Success", - type: "success", - message: "Issue deleted successfully", - }); - - if (issueId && redirection) router.back(); - }) - .catch(() => { - setIsDeleteLoading(false); - }); - if (onSubmit) await onSubmit(); + if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false)); }; - const handleArchivedIssueDeletion = async () => { - setIsDeleteLoading(true); - if (!workspaceSlug || !projectId || !data) return; - - await issueArchiveService - .deleteArchivedIssue(workspaceSlug as string, projectId as string, data.id) - .then(() => { - mutate(PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); - handleClose(); - setToastAlert({ - title: "Success", - type: "success", - message: "Issue deleted successfully", - }); - router.back(); - }) - .catch((error) => { - console.log(error); - setIsDeleteLoading(false); - }); - }; - - const handleIssueDelete = () => (isArchivedIssues ? handleArchivedIssueDeletion() : handleDeletion()); - return ( @@ -194,4 +108,4 @@ export const DeleteIssueModal: React.FC = ({ ); -}; +}); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 778feb57f..9cc95540d 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -9,14 +9,17 @@ import { Spinner } from "@plane/ui"; // types import { ICalendarWeek } from "./types"; import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; type Props = { issues: IIssueGroupedStructure | null; layout: "month" | "week" | undefined; + showWeekends: boolean; + quickActions: (issue: IIssue) => React.ReactNode; }; export const CalendarChart: React.FC = observer((props) => { - const { issues, layout } = props; + const { issues, layout, showWeekends, quickActions } = props; const { calendar: calendarStore } = useMobxStore(); @@ -35,17 +38,17 @@ export const CalendarChart: React.FC = observer((props) => { <>
- +
{layout === "month" ? (
{allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( - + ))}
) : ( - + )}
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 7f49e8431..7d07ce7f4 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -11,11 +11,16 @@ import { renderDateFormat } from "helpers/date-time.helper"; import { IIssueGroupedStructure } from "store/issue"; // constants import { MONTHS_LIST } from "constants/calendar"; +import { IIssue } from "types"; -type Props = { date: ICalendarDate; issues: IIssueGroupedStructure | null }; +type Props = { + date: ICalendarDate; + issues: IIssueGroupedStructure | null; + quickActions: (issue: IIssue) => React.ReactNode; +}; export const CalendarDayTile: React.FC = observer((props) => { - const { date, issues } = props; + const { date, issues, quickActions } = props; const { issueFilter: issueFilterStore } = useMobxStore(); @@ -48,7 +53,7 @@ export const CalendarDayTile: React.FC = observer((props) => { {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} {date.date.getDate()}
- + {provided.placeholder}
diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index 4bb5ebcf0..ba72bfc26 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -1,7 +1,7 @@ -import React from "react"; +import React, { useState } from "react"; import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; - +import { usePopper } from "react-popper"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // icons @@ -14,6 +14,21 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "auto", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + const { activeMonthDate } = calendarStore.calendarFilters; const getWeekLayoutHeader = (): string => { @@ -47,10 +62,17 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { return ( - - {calendarLayout === "month" - ? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}` - : getWeekLayoutHeader()} + + { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - -
+ +
- - - -
-
- {Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => ( - - ))} + + + + +
+
+ {Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => ( -
+ ))} +
-
-
- - ); - }} +
+ + + + )} ); }); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index 915795eba..de58b8622 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,15 +1,17 @@ import Link from "next/link"; import { useRouter } from "next/router"; -import { Draggable } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; - +import { Draggable } from "@hello-pangea/dnd"; // types import { IIssue } from "types"; -type Props = { issues: IIssue[] | null }; +type Props = { + issues: IIssue[] | null; + quickActions: (issue: IIssue) => React.ReactNode; +}; export const CalendarIssueBlocks: React.FC = observer((props) => { - const { issues } = props; + const { issues, quickActions } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -21,7 +23,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { {(provided, snapshot) => ( = observer((props) => { {issue.project_detail.identifier}-{issue.sequence_id}
{issue.name}
+
{quickActions(issue)}
+ {/* handleIssues(issue.target_date ?? "", issue, "delete")} + handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} + /> */}
)} 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 727ce047e..0f0c19f06 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,20 @@ +import { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CalendarChart } from "components/issues"; +import { CalendarChart, CycleIssueQuickActions } from "components/issues"; // types import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; export const CycleCalendarLayout: React.FC = observer(() => { - const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore(); + const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug, cycleId } = router.query; // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -26,12 +31,43 @@ export const CycleCalendarLayout: React.FC = observer(() => { const issues = cycleIssueStore.getIssues; + const handleIssues = useCallback( + (date: string, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !cycleId) return; + + if (action === "update") { + cycleIssueStore.updateIssueStructure(date, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") cycleIssueStore.deleteIssue(date, null, issue); + if (action === "remove" && issue.bridge_id) { + cycleIssueStore.deleteIssue(date, null, issue); + cycleIssueStore.removeIssueFromCycle( + workspaceSlug.toString(), + issue.project, + cycleId.toString(), + issue.bridge_id + ); + } + }, + [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] + ); + return (
( + handleIssues(issue.target_date ?? "", issue, "delete")} + handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} + handleRemoveFromCycle={async () => handleIssues(issue.target_date ?? "", issue, "remove")} + /> + )} />
diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index 48792b5d7..c0afd5a0a 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,15 +1,24 @@ +import { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CalendarChart } from "components/issues"; +import { CalendarChart, ModuleIssueQuickActions } from "components/issues"; // types import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore(); + const { + moduleIssue: moduleIssueStore, + issueFilter: issueFilterStore, + issueDetail: issueDetailStore, + } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug, moduleId } = router.query; // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -26,12 +35,45 @@ export const ModuleCalendarLayout: React.FC = observer(() => { const issues = moduleIssueStore.getIssues; + const handleIssues = useCallback( + (date: string, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !moduleId) return; + + if (action === "update") { + moduleIssueStore.updateIssueStructure(date, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } else { + moduleIssueStore.deleteIssue(date, null, issue); + issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id); + } + if (action === "remove" && issue.bridge_id) { + moduleIssueStore.deleteIssue(date, null, issue); + moduleIssueStore.removeIssueFromModule( + workspaceSlug.toString(), + issue.project, + moduleId.toString(), + issue.bridge_id + ); + } + }, + [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] + ); + return (
( + handleIssues(issue.target_date ?? "", issue, "delete")} + handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} + handleRemoveFromModule={async () => handleIssues(issue.target_date ?? "", issue, "remove")} + /> + )} />
diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index 1482c4262..1d7c1cea3 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,15 +1,20 @@ +import { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CalendarChart } from "components/issues"; +import { CalendarChart, ProjectIssueQuickActions } from "components/issues"; // types import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; export const CalendarLayout: React.FC = observer(() => { - const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); + const { issue: issueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug } = router.query; // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -26,12 +31,35 @@ export const CalendarLayout: React.FC = observer(() => { const issues = issueStore.getIssues; + const handleIssues = useCallback( + (date: string, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + issueStore.updateIssueStructure(date, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } else { + issueStore.deleteIssue(date, null, issue); + issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id); + } + }, + [issueStore, issueDetailStore, workspaceSlug] + ); + return (
( + handleIssues(issue.target_date ?? "", issue, "delete")} + handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} + /> + )} />
diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 76a0ac525..5aa9e1545 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,15 +1,24 @@ +import { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CalendarChart } from "components/issues"; +import { CalendarChart, ProjectIssueQuickActions } from "components/issues"; // types import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; export const ProjectViewCalendarLayout: React.FC = observer(() => { - const { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore } = useMobxStore(); + const { + projectViewIssues: projectViewIssuesStore, + issueFilter: issueFilterStore, + issueDetail: issueDetailStore, + } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug } = router.query; // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -26,12 +35,35 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => { const issues = projectViewIssuesStore.getIssues; + const handleIssues = useCallback( + (date: string, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + projectViewIssuesStore.updateIssueStructure(date, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } else { + projectViewIssuesStore.deleteIssue(date, null, issue); + issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id); + } + }, + [projectViewIssuesStore, issueDetailStore, workspaceSlug] + ); + return (
( + handleIssues(issue.target_date ?? "", issue, "delete")} + handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} + /> + )} />
diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index f6317b3a5..2d6d5d09a 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -9,14 +9,16 @@ import { renderDateFormat } from "helpers/date-time.helper"; // types import { ICalendarDate, ICalendarWeek } from "./types"; import { IIssueGroupedStructure } from "store/issue"; +import { IIssue } from "types"; type Props = { issues: IIssueGroupedStructure | null; week: ICalendarWeek | undefined; + quickActions: (issue: IIssue) => React.ReactNode; }; export const CalendarWeekDays: React.FC = observer((props) => { - const { issues, week } = props; + const { issues, week, quickActions } = props; const { issueFilter: issueFilterStore } = useMobxStore(); @@ -34,7 +36,9 @@ export const CalendarWeekDays: React.FC = observer((props) => { {Object.values(week).map((date: ICalendarDate) => { if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null; - return ; + return ( + + ); })}
); diff --git a/web/components/issues/issue-layouts/calendar/week-header.tsx b/web/components/issues/issue-layouts/calendar/week-header.tsx index eb6b9ae46..52e64e5ae 100644 --- a/web/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/components/issues/issue-layouts/calendar/week-header.tsx @@ -1,21 +1,25 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { DAYS_LIST } from "constants/calendar"; -export const CalendarWeekHeader: React.FC = observer(() => { - const { issueFilter: issueFilterStore } = useMobxStore(); +type Props = { + isLoading: boolean; + showWeekends: boolean; +}; - const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false; +export const CalendarWeekHeader: React.FC = observer((props) => { + const { isLoading, showWeekends } = props; return (
+ {isLoading && ( +
+ )} {Object.values(DAYS_LIST).map((day) => { if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 6a1dd8bc1..09173fec8 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -111,8 +111,8 @@ export const DisplayFiltersSelection: React.FC = observer((props) => {
handleDisplayFiltersUpdate({ diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index 04820f94c..b74fe2761 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -20,6 +20,8 @@ export const FilterGroupBy: React.FC = observer((props) => { const [previewEnabled, setPreviewEnabled] = useState(true); + const activeGroupBy = selectedGroupBy ?? null; + return ( <> = observer((props) => { return ( handleUpdate(groupBy.key)} title={groupBy.title} multiple={false} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx index 8732bc739..a6fa2bf06 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx @@ -18,6 +18,8 @@ export const FilterIssueType: React.FC = observer((props) => { const [previewEnabled, setPreviewEnabled] = React.useState(true); + const activeIssueType = selectedIssueType ?? null; + return ( <> = observer((props) => { {ISSUE_FILTER_OPTIONS.map((issueType) => ( handleUpdate(issueType?.key)} title={issueType.title} multiple={false} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index 18920c514..004d1b6e9 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -19,6 +19,8 @@ export const FilterOrderBy: React.FC = observer((props) => { const [previewEnabled, setPreviewEnabled] = useState(true); + const activeOrderBy = selectedOrderBy ?? "-created_at"; + return ( <> = observer((props) => { {ISSUE_ORDER_BY_OPTIONS.filter((option) => orderByOptions.includes(option.key)).map((orderBy) => ( handleUpdate(orderBy.key)} title={orderBy.title} multiple={false} diff --git a/web/components/issues/issue-layouts/index.ts b/web/components/issues/issue-layouts/index.ts index e9c11aed8..5e6b51931 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -1,5 +1,6 @@ // filters export * from "./filters"; +export * from "./quick-action-dropdowns"; // layouts export * from "./list"; diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index c65680dc1..34f0a550e 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,73 +1,73 @@ -// react beautiful dnd import { Draggable } from "@hello-pangea/dnd"; // components import { KanBanProperties } from "./properties"; +// types +import { IIssue } from "types"; interface IssueBlockProps { sub_group_id: string; columnId: string; - issues: any; + index: number; + issue: IIssue; isDragDisabled: boolean; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; - display_properties: any; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + displayProperties: any; } -export const IssueBlock = ({ - sub_group_id, - columnId, - issues, - isDragDisabled, - handleIssues, - display_properties, -}: IssueBlockProps) => ( - <> - {issues && issues.length > 0 ? ( - <> - {issues.map((issue: any, index: any) => ( - = (props) => { + const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props; + + const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { + if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update"); + }; + + return ( + <> + + {(provided, snapshot) => ( +
- {(provided: any, snapshot: any) => ( -
-
- {display_properties && display_properties?.key && ( -
ONE-{issue.sequence_id}
- )} -
{issue.name}
-
- -
+
+ {quickActions( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !columnId && columnId === "null" ? null : columnId, + issue + )} +
+
+ {displayProperties && displayProperties?.key && ( +
+ {issue.project_detail.identifier}-{issue.sequence_id}
+ )} +
{issue.name}
+
+
- )} - - ))} - - ) : ( - !isDragDisabled && ( -
- {/*
Drop here
*/} -
- ) - )} - -); +
+
+ )} + + + ); +}; diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx new file mode 100644 index 000000000..aeee5c2fc --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -0,0 +1,50 @@ +// components +import { KanbanIssueBlock } from "components/issues"; +import { IIssue } from "types"; + +interface IssueBlocksListProps { + sub_group_id: string; + columnId: string; + issues: IIssue[]; + isDragDisabled: boolean; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; + display_properties: any; +} + +export const KanbanIssueBlocksList: React.FC = (props) => { + const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, display_properties } = props; + + return ( + <> + {issues && issues.length > 0 ? ( + <> + {issues.map((issue, index) => ( + + ))} + + ) : ( + !isDragDisabled && ( +
+ {/*
Drop here
*/} +
+ ) + )} + + ); +}; diff --git a/web/components/issues/issue-layouts/kanban/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/cycle-root.tsx index 1f00585f9..f4d09aada 100644 --- a/web/components/issues/issue-layouts/kanban/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/cycle-root.tsx @@ -1,14 +1,15 @@ -import React from "react"; -// react beautiful dnd -import { DragDropContext } from "@hello-pangea/dnd"; -// mobx +import React, { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { DragDropContext } from "@hello-pangea/dnd"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanSwimLanes } from "./swimlanes"; import { KanBan } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { CycleIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; @@ -20,7 +21,11 @@ export const CycleKanBanLayout: React.FC = observer(() => { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, cycleIssueKanBanView: cycleIssueKanBanViewStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug, cycleId } = router.query; const issues = cycleIssueStore?.getIssues; @@ -50,9 +55,27 @@ export const CycleKanBanLayout: React.FC = observer(() => { : cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; - const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { - cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); - }; + const handleIssues = useCallback( + (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !cycleId) return; + + if (action === "update") { + cycleIssueStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue); + if (action === "remove" && issue.bridge_id) { + cycleIssueStore.deleteIssue(group_by, null, issue); + cycleIssueStore.removeIssueFromCycle( + workspaceSlug.toString(), + issue.project, + cycleId.toString(), + issue.bridge_id + ); + } + }, + [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] + ); const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value); @@ -74,7 +97,15 @@ export const CycleKanBanLayout: React.FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} display_properties={display_properties} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -91,7 +122,15 @@ export const CycleKanBanLayout: React.FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} display_properties={display_properties} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 67aadbef7..691e50fb9 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,16 +1,15 @@ import React from "react"; -// react beautiful dnd +import { observer } from "mobx-react-lite"; import { Droppable } from "@hello-pangea/dnd"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; -import { IssueBlock } from "./block"; +import { KanbanIssueBlocksList } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; -// mobx -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface IGroupByKanBan { issues: any; @@ -20,14 +19,20 @@ export interface IGroupByKanBan { list: any; listKey: string; isDragDisabled: boolean; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; kanBanToggle: any; handleKanBanToggle: any; } -const GroupByKanBan: React.FC = observer( - ({ +const GroupByKanBan: React.FC = observer((props) => { + const { issues, sub_group_by, group_by, @@ -36,74 +41,76 @@ const GroupByKanBan: React.FC = observer( listKey, isDragDisabled, handleIssues, + quickActions, display_properties, kanBanToggle, handleKanBanToggle, - }) => { - const verticalAlignPosition = (_list: any) => - kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string); + } = props; - return ( -
- {list && - list.length > 0 && - list.map((_list: any) => ( -
- {sub_group_by === null && ( -
- -
- )} + const verticalAlignPosition = (_list: any) => + kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string); -
- - {(provided: any, snapshot: any) => ( -
- {issues ? ( - - ) : ( - isDragDisabled && ( -
- {/*
Drop here
*/} -
- ) - )} - {provided.placeholder} -
- )} -
+ return ( +
+ {list && + list.length > 0 && + list.map((_list: any) => ( +
+ {sub_group_by === null && ( +
+
+ )} + +
+ + {(provided: any, snapshot: any) => ( +
+ {issues ? ( + + ) : ( + isDragDisabled && ( +
+ {/*
Drop here
*/} +
+ ) + )} + {provided.placeholder} +
+ )} +
- ))} -
- ); - } -); +
+ ))} +
+ ); +}); export interface IKanBan { issues: any; @@ -111,7 +118,13 @@ export interface IKanBan { group_by: string | null; sub_group_id?: string; handleDragDrop?: (result: any) => void | undefined; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; kanBanToggle: any; handleKanBanToggle: any; @@ -132,6 +145,7 @@ export const KanBan: React.FC = observer((props) => { group_by, sub_group_id = "null", handleIssues, + quickActions, display_properties, kanBanToggle, handleKanBanToggle, @@ -144,7 +158,7 @@ export const KanBan: React.FC = observer((props) => { estimates, } = props; - const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore(); return (
@@ -158,6 +172,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`id`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -174,6 +189,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`key`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -190,6 +206,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`key`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -206,6 +223,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`id`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -222,6 +240,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`member.id`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -238,6 +257,7 @@ export const KanBan: React.FC = observer((props) => { listKey={`member.id`} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/index.ts b/web/components/issues/issue-layouts/kanban/index.ts index a46683af6..3adfe5c26 100644 --- a/web/components/issues/issue-layouts/kanban/index.ts +++ b/web/components/issues/issue-layouts/kanban/index.ts @@ -1,3 +1,5 @@ +export * from "./block"; +export * from "./blocks-list"; export * from "./cycle-root"; export * from "./module-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/kanban/module-root.tsx b/web/components/issues/issue-layouts/kanban/module-root.tsx index 9e42bfe05..594f15757 100644 --- a/web/components/issues/issue-layouts/kanban/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/module-root.tsx @@ -1,14 +1,15 @@ -import React from "react"; -// react beautiful dnd -import { DragDropContext } from "@hello-pangea/dnd"; -// mobx +import React, { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { DragDropContext } from "@hello-pangea/dnd"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanSwimLanes } from "./swimlanes"; import { KanBan } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { ModuleIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; @@ -20,7 +21,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore, moduleIssueKanBanView: moduleIssueKanBanViewStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug, moduleId } = router.query; const issues = moduleIssueStore?.getIssues; @@ -50,9 +55,27 @@ export const ModuleKanBanLayout: React.FC = observer(() => { : moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; - const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { - moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); - }; + const handleIssues = useCallback( + (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !moduleId) return; + + if (action === "update") { + moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue); + if (action === "remove" && issue.bridge_id) { + moduleIssueStore.deleteIssue(group_by, null, issue); + moduleIssueStore.removeIssueFromModule( + workspaceSlug.toString(), + issue.project, + moduleId.toString(), + issue.bridge_id + ); + } + }, + [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] + ); const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value); @@ -74,7 +97,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} display_properties={display_properties} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -91,7 +122,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} display_properties={display_properties} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx index 68089c78f..badcd04aa 100644 --- a/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx @@ -1,15 +1,17 @@ -import { FC } from "react"; -import { DragDropContext } from "@hello-pangea/dnd"; -// mobx +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { DragDropContext } from "@hello-pangea/dnd"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanSwimLanes } from "./swimlanes"; import { KanBan } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { ProjectIssueQuickActions } from "components/issues"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +// types +import { IIssue } from "types"; export interface IProfileIssuesKanBanLayout {} @@ -20,7 +22,11 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { profileIssues: profileIssuesStore, profileIssueFilters: profileIssueFiltersStore, issueKanBanView: issueKanBanViewStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug } = router.query; const issues = profileIssuesStore?.getIssues; @@ -50,9 +56,18 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; - const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { - profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); - }; + const handleIssues = useCallback( + (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") profileIssuesStore.deleteIssue(group_by, sub_group_by, issue); + }, + [profileIssuesStore, issueDetailStore, workspaceSlug] + ); const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { issueKanBanViewStore.handleKanBanToggle(toggle, value); @@ -74,7 +89,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -91,7 +113,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx index d6d4e1625..741e878a0 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/root.tsx @@ -1,24 +1,31 @@ -import { FC } from "react"; +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanSwimLanes } from "./swimlanes"; import { KanBan } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { ProjectIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IKanBanLayout {} export const KanBanLayout: FC = observer(() => { + const router = useRouter(); + const { workspaceSlug } = router.query; + const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore, issueKanBanView: issueKanBanViewStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); const issues = issueStore?.getIssues; @@ -48,9 +55,18 @@ export const KanBanLayout: FC = observer(() => { : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; - const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { - issueStore.updateIssueStructure(group_by, sub_group_by, issue); - }; + const handleIssues = useCallback( + (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + issueStore.updateIssueStructure(group_by, sub_group_by, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") issueStore.deleteIssue(group_by, sub_group_by, issue); + }, + [issueStore, issueDetailStore, workspaceSlug] + ); const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { issueKanBanViewStore.handleKanBanToggle(toggle, value); @@ -72,7 +88,14 @@ export const KanBanLayout: FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -89,7 +112,14 @@ export const KanBanLayout: FC = observer(() => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={updateIssue} + handleIssues={handleIssues} + quickActions={(sub_group_by, group_by, issue) => ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index cb7082d62..9090162c0 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -1,15 +1,15 @@ import React from "react"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBan } from "./default"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; -// mobx -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; interface ISubGroupSwimlaneHeader { issues: any; @@ -61,7 +61,13 @@ const SubGroupSwimlaneHeader: React.FC = ({ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issues: any; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; kanBanToggle: any; handleKanBanToggle: any; @@ -81,6 +87,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { list, listKey, handleIssues, + quickActions, display_properties, kanBanToggle, handleKanBanToggle, @@ -130,6 +137,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { group_by={group_by} sub_group_id={getValueFromObject(_list, listKey) as string} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -153,7 +161,13 @@ export interface IKanBanSwimLanes { issues: any; sub_group_by: string | null; group_by: string | null; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + handleIssues: ( + sub_group_by: string | null, + group_by: string | null, + issue: IIssue, + action: "update" | "delete" + ) => void; + quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; kanBanToggle: any; handleKanBanToggle: any; @@ -172,6 +186,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { sub_group_by, group_by, handleIssues, + quickActions, display_properties, kanBanToggle, handleKanBanToggle, @@ -184,7 +199,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { estimates, } = props; - const { project: projectStore }: RootStore = useMobxStore(); + const { project: projectStore } = useMobxStore(); return (
@@ -270,6 +285,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={projectStore?.projectStates} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -291,6 +307,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={ISSUE_STATE_GROUPS} listKey={`key`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -312,6 +329,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={ISSUE_PRIORITIES} listKey={`key`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -333,6 +351,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={projectStore?.projectLabels} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -354,6 +373,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={projectStore?.projectMembers} listKey={`member.id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} @@ -375,6 +395,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { list={projectStore?.projectMembers} listKey={`member.id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} diff --git a/web/components/issues/issue-layouts/kanban/view-root.tsx b/web/components/issues/issue-layouts/kanban/view-root.tsx index bdc10f1a1..78117da80 100644 --- a/web/components/issues/issue-layouts/kanban/view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/view-root.tsx @@ -62,45 +62,47 @@ export const ViewKanBanLayout: React.FC = observer(() => { const projects = projectStore?.projectStates || null; const estimates = null; - return ( -
- - {currentKanBanView === "default" ? ( - {}} - handleKanBanToggle={() => {}} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members} - projects={projects} - estimates={estimates} - /> - ) : ( - {}} - handleKanBanToggle={() => {}} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members} - projects={projects} - estimates={estimates} - /> - )} - -
- ); + return null; + + // return ( + //
+ // + // {currentKanBanView === "default" ? ( + // {}} + // handleKanBanToggle={() => {}} + // states={states} + // stateGroups={stateGroups} + // priorities={priorities} + // labels={labels} + // members={members} + // projects={projects} + // estimates={estimates} + // /> + // ) : ( + // {}} + // handleKanBanToggle={() => {}} + // states={states} + // stateGroups={stateGroups} + // priorities={priorities} + // labels={labels} + // members={members} + // projects={projects} + // estimates={estimates} + // /> + // )} + // + //
+ // ); }); diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index a542f5983..47a1a38f0 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,14 +1,16 @@ -import { FC } from "react"; // components import { KanBanProperties } from "./properties"; import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui import { Tooltip } from "@plane/ui"; +// types +import { IIssue } from "types"; interface IssueBlockProps { columnId: string; - issues: any; - handleIssues?: (group_by: string | null, issue: any) => void; + issue: IIssue; + handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; states: any; labels: any; @@ -16,53 +18,48 @@ interface IssueBlockProps { priorities: any; } -export const IssueBlock: FC = (props) => { - const { columnId, issues, handleIssues, display_properties, states, labels, members, priorities } = props; +export const IssueBlock: React.FC = (props) => { + const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, priorities } = + props; - const handleIssue = (_issue: any) => { - if (_issue && handleIssues) handleIssues(!columnId && columnId === "null" ? null : columnId, _issue); + const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { + if (issueToUpdate && handleIssues) handleIssues(group_by, issueToUpdate, "update"); }; return ( <> - {issues && - issues?.length > 0 && - issues.map((issue: any, index: any) => ( -
- {display_properties && display_properties?.key && ( -
- {issue?.project_detail?.identifier}-{issue.sequence_id} -
- )} - - - -
{issue.name}
-
-
- -
- -
+
+ {display_properties && display_properties?.key && ( +
+ {issue?.project_detail?.identifier}-{issue.sequence_id}
- ))} + )} + {}} + > + +
{issue.name}
+
+
+ +
+ + {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} +
+
); }; diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx new file mode 100644 index 000000000..33618505f --- /dev/null +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -0,0 +1,43 @@ +import { FC } from "react"; +// components +import { IssueBlock } from "components/issues"; +// types +import { IIssue } from "types"; + +interface Props { + columnId: string; + issues: IIssue[]; + handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + display_properties: any; + states: any; + labels: any; + members: any; + priorities: any; +} + +export const IssueBlocksList: FC = (props) => { + const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, priorities } = + props; + + return ( + <> + {issues && + issues?.length > 0 && + issues.map((issue) => ( + + ))} + + ); +}; diff --git a/web/components/issues/issue-layouts/list/cycle-root.tsx b/web/components/issues/issue-layouts/list/cycle-root.tsx index 389656950..511336531 100644 --- a/web/components/issues/issue-layouts/list/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/cycle-root.tsx @@ -1,21 +1,28 @@ -import React from "react"; +import React, { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { List } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { CycleIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, cycleId } = router.query; + const { project: projectStore, issueFilter: issueFilterStore, cycleIssue: cycleIssueStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); const issues = cycleIssueStore?.getIssues; @@ -23,9 +30,27 @@ export const CycleListLayout: React.FC = observer(() => { const display_properties = issueFilterStore?.userDisplayProperties || null; - const updateIssue = (group_by: string | null, issue: any) => { - cycleIssueStore.updateIssueStructure(group_by, null, issue); - }; + const handleIssues = useCallback( + (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !cycleId) return; + + if (action === "update") { + cycleIssueStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue); + if (action === "remove" && issue.bridge_id) { + cycleIssueStore.deleteIssue(group_by, null, issue); + cycleIssueStore.removeIssueFromCycle( + workspaceSlug.toString(), + issue.project, + cycleId.toString(), + issue.bridge_id + ); + } + }, + [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] + ); const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; @@ -40,7 +65,15 @@ export const CycleListLayout: React.FC = observer(() => { ( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")} + /> + )} display_properties={display_properties} states={states} stateGroups={stateGroups} diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index b3ec8a009..008e625ae 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -5,13 +5,16 @@ import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlock } from "./block"; // constants import { getValueFromObject } from "constants/issue"; +import { IIssue } from "types"; +import { IssueBlocksList } from "./blocks-list"; export interface IGroupByList { issues: any; group_by: string | null; list: any; listKey: string; - handleIssues?: (group_by: string | null, issue: any) => void; + handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; is_list?: boolean; states: any; @@ -30,6 +33,7 @@ const GroupByList: React.FC = observer((props) => { list, listKey, handleIssues, + quickActions, display_properties, is_list = false, states, @@ -59,10 +63,11 @@ const GroupByList: React.FC = observer((props) => {
{issues && ( - = observer((props) => { ); }); +// TODO: update all the types export interface IList { issues: any; group_by: string | null; handleDragDrop?: (result: any) => void | undefined; - handleIssues?: (group_by: string | null, issue: any) => void; + handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; states: any; labels: any; @@ -97,6 +104,7 @@ export const List: React.FC = observer((props) => { issues, group_by, handleIssues, + quickActions, display_properties, states, labels, @@ -116,6 +124,7 @@ export const List: React.FC = observer((props) => { list={[{ id: "null", title: "All Issues" }]} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} is_list={true} states={states} @@ -135,6 +144,7 @@ export const List: React.FC = observer((props) => { list={projects} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -153,6 +163,7 @@ export const List: React.FC = observer((props) => { list={states} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -171,6 +182,7 @@ export const List: React.FC = observer((props) => { list={stateGroups} listKey={`key`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -189,6 +201,7 @@ export const List: React.FC = observer((props) => { list={priorities} listKey={`key`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -207,6 +220,7 @@ export const List: React.FC = observer((props) => { list={labels} listKey={`id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -225,6 +239,7 @@ export const List: React.FC = observer((props) => { list={members} listKey={`member.id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} @@ -243,6 +258,7 @@ export const List: React.FC = observer((props) => { list={members} listKey={`member.id`} handleIssues={handleIssues} + quickActions={quickActions} display_properties={display_properties} states={states} labels={labels} diff --git a/web/components/issues/issue-layouts/list/index.ts b/web/components/issues/issue-layouts/list/index.ts index a46683af6..3adfe5c26 100644 --- a/web/components/issues/issue-layouts/list/index.ts +++ b/web/components/issues/issue-layouts/list/index.ts @@ -1,3 +1,5 @@ +export * from "./block"; +export * from "./blocks-list"; export * from "./cycle-root"; export * from "./module-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/list/module-root.tsx b/web/components/issues/issue-layouts/list/module-root.tsx index 863467b56..485e4e908 100644 --- a/web/components/issues/issue-layouts/list/module-root.tsx +++ b/web/components/issues/issue-layouts/list/module-root.tsx @@ -1,21 +1,28 @@ -import React from "react"; +import React, { useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { List } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { ModuleIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, moduleId } = router.query; + const { project: projectStore, issueFilter: issueFilterStore, moduleIssue: moduleIssueStore, - }: RootStore = useMobxStore(); + issueDetail: issueDetailStore, + } = useMobxStore(); const issues = moduleIssueStore?.getIssues; @@ -23,9 +30,27 @@ export const ModuleListLayout: React.FC = observer(() => { const display_properties = issueFilterStore?.userDisplayProperties || null; - const updateIssue = (group_by: string | null, issue: any) => { - moduleIssueStore.updateIssueStructure(group_by, null, issue); - }; + const handleIssues = useCallback( + (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + if (!workspaceSlug || !moduleId) return; + + if (action === "update") { + moduleIssueStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") moduleIssueStore.deleteIssue(group_by, null, issue); + if (action === "remove" && issue.bridge_id) { + moduleIssueStore.deleteIssue(group_by, null, issue); + moduleIssueStore.removeIssueFromModule( + workspaceSlug.toString(), + issue.project, + moduleId.toString(), + issue.bridge_id + ); + } + }, + [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] + ); const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; @@ -40,7 +65,15 @@ export const ModuleListLayout: React.FC = observer(() => { ( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")} + /> + )} display_properties={display_properties} states={states} stateGroups={stateGroups} diff --git a/web/components/issues/issue-layouts/list/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/profile-issues-root.tsx index 5ee9958aa..b1fb86c6a 100644 --- a/web/components/issues/issue-layouts/list/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/profile-issues-root.tsx @@ -1,10 +1,13 @@ -import { FC } from "react"; +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { List } from "./default"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { ProjectIssueQuickActions } from "components/issues"; +// types +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; @@ -15,18 +18,31 @@ export const ProfileIssuesListLayout: FC = observer(() => { workspace: workspaceStore, project: projectStore, profileIssueFilters: profileIssueFiltersStore, - profileIssues: profileIssuesIssueStore, - }: RootStore = useMobxStore(); + profileIssues: profileIssuesStore, + issueDetail: issueDetailStore, + } = useMobxStore(); - const issues = profileIssuesIssueStore?.getIssues; + const router = useRouter(); + const { workspaceSlug } = router.query; + + const issues = profileIssuesStore?.getIssues; const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; - const updateIssue = (group_by: string | null, issue: any) => { - profileIssuesIssueStore.updateIssueStructure(group_by, null, issue); - }; + const handleIssues = useCallback( + (group_by: string | null, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + profileIssuesStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") profileIssuesStore.deleteIssue(group_by, null, issue); + }, + [profileIssuesStore, issueDetailStore, workspaceSlug] + ); const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; @@ -41,7 +57,14 @@ export const ProfileIssuesListLayout: FC = observer(() => { ( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + /> + )} display_properties={display_properties} states={states} stateGroups={stateGroups} diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 953b6ba4d..86af82656 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -10,11 +10,13 @@ import { IssuePropertyEstimates } from "../properties/estimates"; import { IssuePropertyDate } from "../properties/date"; // ui import { Tooltip } from "@plane/ui"; +// types +import { IIssue } from "types"; export interface IKanBanProperties { columnId: string; issue: any; - handleIssues?: (group_by: string | null, issue: any) => void; + handleIssues?: (group_by: string | null, issue: IIssue) => void; display_properties: any; states: any; labels: any; diff --git a/web/components/issues/issue-layouts/list/root.tsx b/web/components/issues/issue-layouts/list/root.tsx index e78159978..a5b1baa64 100644 --- a/web/components/issues/issue-layouts/list/root.tsx +++ b/web/components/issues/issue-layouts/list/root.tsx @@ -1,16 +1,26 @@ -import { FC } from "react"; +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// components -import { List } from "./default"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { List } from "./default"; +import { ProjectIssueQuickActions } from "components/issues"; // types -import { RootStore } from "store/root"; +import { IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export const ListLayout: FC = observer(() => { - const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + project: projectStore, + issue: issueStore, + issueDetail: issueDetailStore, + issueFilter: issueFilterStore, + } = useMobxStore(); const issues = issueStore?.getIssues; @@ -18,9 +28,18 @@ export const ListLayout: FC = observer(() => { const display_properties = issueFilterStore?.userDisplayProperties || null; - const updateIssue = (group_by: string | null, issue: any) => { - issueStore.updateIssueStructure(group_by, null, issue); - }; + const handleIssues = useCallback( + (group_by: string | null, issue: IIssue, action: "update" | "delete") => { + if (!workspaceSlug) return; + + if (action === "update") { + issueStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + } + if (action === "delete") issueStore.deleteIssue(group_by, null, issue); + }, + [issueStore, issueDetailStore, workspaceSlug] + ); const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; @@ -31,11 +50,18 @@ export const ListLayout: FC = observer(() => { const estimates = null; return ( -
+
( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + /> + )} display_properties={display_properties} states={states} stateGroups={stateGroups} diff --git a/web/components/issues/issue-layouts/list/view-root.tsx b/web/components/issues/issue-layouts/list/view-root.tsx index daf499045..aa7ab563a 100644 --- a/web/components/issues/issue-layouts/list/view-root.tsx +++ b/web/components/issues/issue-layouts/list/view-root.tsx @@ -31,21 +31,23 @@ export const ViewListLayout: React.FC = observer(() => { const projects = projectStore?.projectStates || null; const estimates = null; - return ( -
- -
- ); + return null; + + // return ( + //
+ // + //
+ // ); }); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx new file mode 100644 index 000000000..98e58600b --- /dev/null +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -0,0 +1,130 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { CustomMenu } from "@plane/ui"; +import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// types +import { IIssue } from "types"; + +type Props = { + issue: IIssue; + handleDelete: () => Promise; + handleUpdate: (data: IIssue) => Promise; + handleRemoveFromCycle: () => Promise; +}; + +export const CycleIssueQuickActions: React.FC = (props) => { + const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + // states + const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState(null); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const { setToastAlert } = useToast(); + + const handleCopyIssueLink = () => { + copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + setToastAlert({ + type: "success", + title: "Link copied", + message: "Issue link copied to clipboard", + }) + ); + }; + + return ( + <> + setDeleteIssueModal(false)} + onSubmit={handleDelete} + /> + { + setCreateUpdateIssueModal(false); + setIssueToEdit(null); + }} + // pre-populate date only if not editing + prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} + data={issueToEdit} + onSubmit={async (data) => { + if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + }} + /> + + { + e.preventDefault(); + e.stopPropagation(); + handleCopyIssueLink(); + }} + > +
+ + Copy link +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit issue +
+
+ { + e.preventDefault(); + e.stopPropagation(); + handleRemoveFromCycle(); + }} + > +
+ + Remove from cycle +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setDeleteIssueModal(true); + }} + > +
+ + Delete issue +
+
+
+ + ); +}; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/components/issues/issue-layouts/quick-action-dropdowns/index.ts new file mode 100644 index 000000000..0d281a2a3 --- /dev/null +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -0,0 +1,3 @@ +export * from "./cycle-issue"; +export * from "./module-issue"; +export * from "./project-issue"; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx new file mode 100644 index 000000000..7f37285b9 --- /dev/null +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -0,0 +1,130 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { CustomMenu } from "@plane/ui"; +import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// types +import { IIssue } from "types"; + +type Props = { + issue: IIssue; + handleDelete: () => Promise; + handleUpdate: (data: IIssue) => Promise; + handleRemoveFromModule: () => Promise; +}; + +export const ModuleIssueQuickActions: React.FC = (props) => { + const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + // states + const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState(null); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const { setToastAlert } = useToast(); + + const handleCopyIssueLink = () => { + copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + setToastAlert({ + type: "success", + title: "Link copied", + message: "Issue link copied to clipboard", + }) + ); + }; + + return ( + <> + setDeleteIssueModal(false)} + onSubmit={handleDelete} + /> + { + setCreateUpdateIssueModal(false); + setIssueToEdit(null); + }} + // pre-populate date only if not editing + prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} + data={issueToEdit} + onSubmit={async (data) => { + if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + }} + /> + + { + e.preventDefault(); + e.stopPropagation(); + handleCopyIssueLink(); + }} + > +
+ + Copy link +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit issue +
+
+ { + e.preventDefault(); + e.stopPropagation(); + handleRemoveFromModule(); + }} + > +
+ + Remove from module +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setDeleteIssueModal(true); + }} + > +
+ + Delete issue +
+
+
+ + ); +}; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx new file mode 100644 index 000000000..00005a02e --- /dev/null +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { CustomMenu } from "@plane/ui"; +import { Copy, Link, Pencil, Trash2 } from "lucide-react"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// types +import { IIssue } from "types"; + +type Props = { + issue: IIssue; + handleDelete: () => Promise; + handleUpdate: (data: IIssue) => Promise; +}; + +export const ProjectIssueQuickActions: React.FC = (props) => { + const { issue, handleDelete, handleUpdate } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + // states + const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState(null); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const { setToastAlert } = useToast(); + + const handleCopyIssueLink = () => { + copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + setToastAlert({ + type: "success", + title: "Link copied", + message: "Issue link copied to clipboard", + }) + ); + }; + + return ( + <> + setDeleteIssueModal(false)} + onSubmit={handleDelete} + /> + { + setCreateUpdateIssueModal(false); + setIssueToEdit(null); + }} + // pre-populate date only if not editing + prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} + data={issueToEdit} + onSubmit={async (data) => { + if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + }} + /> + + { + e.preventDefault(); + e.stopPropagation(); + handleCopyIssueLink(); + }} + > +
+ + Copy link +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit issue +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setDeleteIssueModal(true); + }} + > +
+ + Delete issue +
+
+
+ + ); +}; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx index 41de853b1..8bb5235c9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { Popover2 } from "@blueprintjs/popover2"; -import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react"; +import { MoreHorizontal, Pencil, Trash2, ChevronRight, Link } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; // helpers @@ -82,6 +82,20 @@ export const IssueColumn: React.FC = ({ onInteraction={(nextOpenState) => setIsOpen(nextOpenState)} content={
+ +
- -
} placement="bottom-start" diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx index f9ea0237b..887b7bae4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx @@ -45,7 +45,7 @@ export const SpreadsheetIssuesColumn: React.FC = ({ const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); return ( -
+ <> = ({ !isLoading && subIssues && subIssues.length > 0 && - subIssues.map((subIssue: IIssue) => ( + subIssues.map((subIssue) => ( = ({ nestingLevel={nestingLevel + 1} /> ))} -
+ ); }; diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index c5b75d002..43c53e51c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -20,10 +20,8 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { cycleIssue: cycleIssueStore, issueDetail: issueDetailStore, project: projectStore, - user: userStore, } = useMobxStore(); - const user = userStore.currentUser; const issues = cycleIssueStore.getIssues; const handleDisplayFiltersUpdate = useCallback( @@ -41,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { const handleUpdateIssue = useCallback( (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId || !user) return; + if (!workspaceSlug || !projectId) return; const payload = { ...issue, @@ -49,9 +47,9 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { }; cycleIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); }, - [issueDetailStore, cycleIssueStore, projectId, user, workspaceSlug] + [issueDetailStore, cycleIssueStore, projectId, workspaceSlug] ); return ( diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index f5b222d74..cfa4259e7 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -20,10 +20,8 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { moduleIssue: moduleIssueStore, issueDetail: issueDetailStore, project: projectStore, - user: userStore, } = useMobxStore(); - const user = userStore.currentUser; const issues = moduleIssueStore.getIssues; const handleDisplayFiltersUpdate = useCallback( @@ -41,7 +39,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { const handleUpdateIssue = useCallback( (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId || !user) return; + if (!workspaceSlug || !projectId) return; const payload = { ...issue, @@ -49,9 +47,9 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { }; moduleIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); }, - [issueDetailStore, moduleIssueStore, projectId, user, workspaceSlug] + [issueDetailStore, moduleIssueStore, projectId, workspaceSlug] ); return ( diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index 0341bb241..78940ceaf 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -49,7 +49,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => { }; issueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); }, [issueStore, issueDetailStore, projectId, user, workspaceSlug] ); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index 509c9de62..80092be5e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -20,10 +20,8 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { projectViewIssues: projectViewIssueStore, issueDetail: issueDetailStore, project: projectStore, - user: userStore, } = useMobxStore(); - const user = userStore.currentUser; const issues = projectViewIssueStore.getIssues; const handleDisplayFiltersUpdate = useCallback( @@ -41,7 +39,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { const handleUpdateIssue = useCallback( (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId || !user) return; + if (!workspaceSlug || !projectId) return; const payload = { ...issue, @@ -49,9 +47,9 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { }; projectViewIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); }, - [issueDetailStore, projectViewIssueStore, projectId, user, workspaceSlug] + [issueDetailStore, projectViewIssueStore, projectId, workspaceSlug] ); return ( diff --git a/web/components/issues/issue-peek-overview/root.tsx b/web/components/issues/issue-peek-overview/root.tsx index 40a742f3d..6df455436 100644 --- a/web/components/issues/issue-peek-overview/root.tsx +++ b/web/components/issues/issue-peek-overview/root.tsx @@ -30,7 +30,7 @@ export const IssuePeekOverview: FC = observer((props) => { const issueUpdate = (_data: Partial) => { if (handleIssue) { handleIssue(_data); - issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data, undefined); + issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data); } }; diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index b10d16ab4..3d8a53139 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -16,13 +16,12 @@ import { IssueForm, ConfirmIssueDiscard } from "components/issues"; // types import type { IIssue } from "types"; // fetch-keys -import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; +import { USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; export interface IssuesModalProps { data?: IIssue | null; handleClose: () => void; isOpen: boolean; - isUpdatingSingleIssue?: boolean; prePopulateData?: Partial; fieldsToShow?: ( | "project" @@ -46,15 +45,7 @@ const issueService = new IssueService(); const issueDraftService = new IssueDraftService(); export const CreateUpdateIssueModal: React.FC = observer((props) => { - const { - data, - handleClose, - isOpen, - isUpdatingSingleIssue = false, - prePopulateData: prePopulateDataProps, - fieldsToShow = ["all"], - onSubmit, - } = props; + const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props; // states const [createMore, setCreateMore] = useState(false); @@ -211,10 +202,10 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }; const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; await issueDetailStore - .createIssue(workspaceSlug.toString(), activeProject, payload, user) + .createIssue(workspaceSlug.toString(), activeProject, payload) .then(async (res) => { issueStore.fetchIssues(workspaceSlug.toString(), activeProject); @@ -280,16 +271,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop await issueService .patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) - .then((res) => { - if (isUpdatingSingleIssue) { - mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); - } else { - if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); - } - - if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); - + .then(() => { if (!createMore) onFormSubmitClose(); setToastAlert({ diff --git a/web/components/issues/my-issues/my-issues-view.tsx b/web/components/issues/my-issues/my-issues-view.tsx index d968e2fcc..9ec77cbbf 100644 --- a/web/components/issues/my-issues/my-issues-view.tsx +++ b/web/components/issues/my-issues/my-issues-view.tsx @@ -6,7 +6,6 @@ import { IssueLabelService } from "services/issue"; // hooks import useMyIssues from "hooks/my-issues/use-my-issues"; import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; -import useUserAuth from "hooks/use-user-auth"; // components import { FiltersList } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; @@ -43,8 +42,6 @@ export const MyIssuesView: React.FC = () => { const router = useRouter(); const { workspaceSlug } = router.query; - const { user } = useUserAuth(); - const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); @@ -220,15 +217,16 @@ export const MyIssuesView: React.FC = () => { mutateMyIssues(); }} /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateMyIssues(); - }} - /> + {issueToDelete && ( + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + onSubmit={async () => { + mutateMyIssues(); + }} + /> + )} {areFiltersApplied && ( <>
diff --git a/web/components/issues/peek-overview/layout.tsx b/web/components/issues/peek-overview/layout.tsx index cfb8f0683..1ad0104e2 100644 --- a/web/components/issues/peek-overview/layout.tsx +++ b/web/components/issues/peek-overview/layout.tsx @@ -8,8 +8,6 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Dialog, Transition } from "@headlessui/react"; -// hooks -import useUser from "hooks/use-user"; // components import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues"; // types @@ -40,8 +38,6 @@ export const IssuePeekOverview: React.FC = observer(({ handleMutation, pr const issue = issues[peekIssue?.toString() ?? ""]; - const { user } = useUser(); - const handleClose = () => { const { query } = router; delete query.peekIssue; @@ -53,17 +49,17 @@ export const IssuePeekOverview: React.FC = observer(({ handleMutation, pr }; const handleUpdateIssue = async (formData: Partial) => { - if (!issue || !user) return; + if (!issue) return; - await updateIssue(workspaceSlug, projectId, issue.id, formData, user); + await updateIssue(workspaceSlug, projectId, issue.id, formData); mutate(PROJECT_ISSUES_ACTIVITY(issue.id)); if (handleMutation) handleMutation(); }; const handleDeleteIssue = async () => { - if (!issue || !user) return; + if (!issue) return; - await deleteIssue(workspaceSlug, projectId, issue.id, user); + await deleteIssue(workspaceSlug, projectId, issue.id); if (handleMutation) handleMutation(); handleClose(); @@ -92,13 +88,14 @@ export const IssuePeekOverview: React.FC = observer(({ handleMutation, pr return ( <> - setDeleteIssueModal(false)} - data={issue ? { ...issue } : null} - onSubmit={handleDeleteIssue} - user={user} - /> + {issue && ( + setDeleteIssueModal(false)} + data={issue} + onSubmit={handleDeleteIssue} + /> + )}
diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index d92b58b98..075c43cc3 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -277,12 +277,9 @@ export const IssueDetailsSidebar: React.FC = ({ createIssueLink={handleCreateLink} updateIssueLink={handleUpdateLink} /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueDetail ?? null} - user={user} - /> + {issueDetail && ( + setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetail} /> + )}

diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index ceba65959..abe334f16 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -335,18 +335,19 @@ export const SubIssuesRoot: React.FC = ({ parentIssue, user }) = /> )} - {isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && ( - { - mutateSubIssues(issueCrudOperation?.delete?.issueId); - handleIssueCrudOperation("delete", null, null); - }} - data={issueCrudOperation?.delete?.issue} - user={user} - redirection={false} - /> - )} + {isEditable && + issueCrudOperation?.delete?.toggle && + issueCrudOperation?.delete?.issueId && + issueCrudOperation?.delete?.issue && ( + { + mutateSubIssues(issueCrudOperation?.delete?.issueId); + handleIssueCrudOperation("delete", null, null); + }} + data={issueCrudOperation?.delete?.issue} + /> + )} )}

diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 616806368..095fc9722 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,9 +1,12 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; import DatePicker from "react-datepicker"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { ModuleService } from "services/module.service"; // contexts @@ -14,6 +17,7 @@ import useToast from "hooks/use-toast"; import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; +// ui import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui"; // icon import { @@ -29,9 +33,9 @@ import { } from "lucide-react"; // helpers import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; -import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; +import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper"; // types -import { IUser, IIssue, linkDetails, IModule, ModuleLink } from "types"; +import { linkDetails, IModule, ModuleLink } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; // constant @@ -46,21 +50,28 @@ const defaultValues: Partial = { }; type Props = { - module?: IModule; isOpen: boolean; - moduleIssues?: IIssue[]; - user: IUser | undefined; + moduleId: string; }; +// services const moduleService = new ModuleService(); -export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIssues, user }) => { +// TODO: refactor this component +export const ModuleDetailsSidebar: React.FC = observer((props) => { + const { isOpen, moduleId } = props; + const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query; + const { workspaceSlug, projectId } = router.query; + + const { module: moduleStore, user: userStore } = useMobxStore(); + + const user = userStore.currentUser ?? undefined; + const moduleDetails = moduleStore.moduleDetails[moduleId] ?? undefined; const { memberRole } = useProjectMyMembership(); @@ -117,7 +128,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs const payload = { metadata: {}, ...formData }; - const updatedLinks = module.link_module.map((l) => + const updatedLinks = moduleDetails.link_module.map((l) => l.id === linkId ? { ...l, @@ -146,7 +157,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs const handleDeleteLink = async (linkId: string) => { if (!workspaceSlug || !projectId || !module) return; - const updatedLinks = module.link_module.filter((l) => l.id !== linkId); + const updatedLinks = moduleDetails.link_module.filter((l) => l.id !== linkId); mutate( MODULE_DETAILS(module.id), @@ -165,41 +176,45 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs }; const handleCopyText = () => { - // const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`) + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`) .then(() => { setToastAlert({ type: "success", - title: "Module link copied to clipboard", + title: "Link copied", + message: "Module link copied to clipboard", }); }) .catch(() => { setToastAlert({ type: "error", - title: "Some error occurred", + title: "Error!", + message: "Some error occurred", }); }); }; useEffect(() => { - if (module) + if (moduleDetails) reset({ - ...module, - members_list: module.members_list ?? module.members_detail?.map((m) => m.id), + ...moduleDetails, + members_list: moduleDetails.members_list ?? moduleDetails.members_detail?.map((m) => m.id), }); - }, [module, reset]); + }, [moduleDetails, reset]); - const isStartValid = new Date(`${module?.start_date}`) <= new Date(); - const isEndValid = new Date(`${module?.target_date}`) >= new Date(`${module?.start_date}`); + const isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date(); + const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`); - const progressPercentage = module ? Math.round((module.completed_issues / module.total_issues) * 100) : null; + const progressPercentage = moduleDetails + ? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100) + : null; const handleEditLink = (link: linkDetails) => { setSelectedLinkToUpdate(link); setModuleLinkModal(true); }; + if (!moduleDetails) return null; + return ( <> = ({ module, isOpen, moduleIs createIssueLink={handleCreateLink} updateIssueLink={handleUpdateLink} /> - +
= ({ module, isOpen, moduleIs <> - {renderShortDateWithYearFormat(new Date(`${module.start_date}`), "Start date")} + + {renderShortDateWithYearFormat(new Date(`${moduleDetails.start_date}`), "Start date")} + = ({ module, isOpen, moduleIs <> - {renderShortDateWithYearFormat(new Date(`${module?.target_date}`), "End date")} + + {renderShortDateWithYearFormat(new Date(`${moduleDetails?.target_date}`), "End date")} + = ({ module, isOpen, moduleIs
-

{module.name}

+

+ {moduleDetails.name} +

setModuleDeleteModal(true)}> @@ -361,7 +382,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- {module.description} + {moduleDetails.description}
@@ -399,9 +420,9 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- + - {module.completed_issues}/{module.total_issues} + {moduleDetails.completed_issues}/{moduleDetails.total_issues}
@@ -415,7 +436,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
Progress - {!open && moduleIssues && progressPercentage ? ( + {!open && progressPercentage ? ( {progressPercentage ? `${progressPercentage}%` : ""} @@ -439,7 +460,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- {isStartValid && isEndValid && moduleIssues ? ( + {isStartValid && isEndValid ? (
@@ -448,7 +469,8 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs Pending Issues -{" "} - {module.total_issues - (module.completed_issues + module.cancelled_issues)}{" "} + {moduleDetails.total_issues - + (moduleDetails.completed_issues + moduleDetails.cancelled_issues)}{" "}
@@ -465,10 +487,10 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
@@ -491,7 +513,7 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs Other Information
- {module.total_issues > 0 ? ( + {moduleDetails.total_issues > 0 ? ( @@ -506,20 +528,20 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- {module.total_issues > 0 ? ( + {moduleDetails.total_issues > 0 ? ( <>
@@ -544,9 +566,9 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs
- {memberRole && module.link_module && module.link_module.length > 0 ? ( + {memberRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( = ({ module, isOpen, moduleIs
); -}; +}); diff --git a/web/components/profile/profile-issues-view.tsx b/web/components/profile/profile-issues-view.tsx index 6456b8f84..7c4723537 100644 --- a/web/components/profile/profile-issues-view.tsx +++ b/web/components/profile/profile-issues-view.tsx @@ -230,15 +230,16 @@ export const ProfileIssuesView = () => { mutateProfileIssues(); }} /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateProfileIssues(); - }} - /> + {issueToDelete && ( + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + onSubmit={async () => { + mutateProfileIssues(); + }} + /> + )} {areFiltersApplied && ( <>
diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 8090580b9..6596f1d69 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -56,6 +56,19 @@ export const copyTextToClipboard = async (text: string) => { await navigator.clipboard.writeText(text); }; +/** + * @description: This function copies the url to clipboard after prepending the origin URL to it + * @param {string} path + * @example: + * const text = copyUrlToClipboard("path"); + * copied URL: origin_url/path + */ +export const copyUrlToClipboard = async (path: string) => { + const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + + await copyTextToClipboard(`${originUrl}/${path}`); +}; + export const generateRandomColor = (string: string): string => { if (!string) return "rgb(var(--color-primary-100))"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 612b4c813..a558f8a37 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,65 +1,40 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import Link from "next/link"; import useSWR from "swr"; -// icons -import { ArrowLeft } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; // layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; -// contexts -import { IssueViewContextProvider } from "contexts/issue-view.context"; +import { AppLayout } from "layouts/app-layout"; // components +import { CycleIssuesHeader } from "components/headers"; import { ExistingIssuesListModal } from "components/core"; import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles"; import { CycleLayoutRoot } from "components/issues/issue-layouts"; -// services -import { IssueService } from "services/issue"; -import { CycleService } from "services/cycle.service"; -// hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; // ui -import { BreadcrumbItem, Breadcrumbs, CustomMenu, ContrastIcon } from "@plane/ui"; import { EmptyState } from "components/common"; -// images +// assets import emptyCycle from "public/empty-state/cycle.svg"; // helpers -import { truncateText } from "helpers/string.helper"; import { getDateRangeStatus } from "helpers/date-time.helper"; -// types -import { ISearchIssueResponse } from "types"; -// fetch-keys -import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys"; -import { CycleIssuesHeader } from "components/headers"; - -// services -const issueService = new IssueService(); -const cycleService = new CycleService(); const SingleCycle: React.FC = () => { const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - const [cycleSidebar, setCycleSidebar] = useState(true); - const [analyticsModal, setAnalyticsModal] = useState(false); const [transferIssuesModal, setTransferIssuesModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { user } = useUserAuth(); + const { cycle: cycleStore } = useMobxStore(); - const { setToastAlert } = useToast(); - - const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") - : null - ); + const { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); + const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const { data: cycleDetails, error } = useSWR( - workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null, + workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId - ? () => cycleService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) + ? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) : null ); @@ -68,119 +43,59 @@ const SingleCycle: React.FC = () => { ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) : "draft"; - const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; + // TODO: add this function to bulk add issues to cycle + // const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { + // if (!workspaceSlug || !projectId) return; - const payload = { - issues: data.map((i) => i.id), - }; + // const payload = { + // issues: data.map((i) => i.id), + // }; - await issueService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); - }; + // await issueService + // .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) + // .catch(() => { + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Selected issues could not be added to the cycle. Please try again.", + // }); + // }); + // }; return ( - + } withProjectWrapper> + {/* TODO: Update logic to bulk add issues to a cycle */} setCycleIssuesListModal(false)} searchParams={{ cycle: true }} - handleOnSubmit={handleAddIssuesToCycle} + handleOnSubmit={async () => {}} /> - router.back()}> - - -

{`${truncateText( - cycleDetails?.project_detail.name ?? "Project", - 32 - )} Cycles`}

-
- - } - /> - - } - left={ - - - {cycleDetails?.name && truncateText(cycleDetails.name, 40)} - - } - className="ml-1.5 flex-shrink-0" - width="auto" - > - {cycles?.map((cycle) => ( - router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} - > - {truncateText(cycle.name, 40)} - - ))} - - } - right={ -
- - -
- } - > - {error ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/cycles`), - }} - /> - ) : ( - <> - setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> - -
+ {error ? ( + router.push(`/${workspaceSlug}/projects/${projectId}/cycles`), + }} + /> + ) : ( + <> + setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> +
+
{cycleStatus === "completed" && setTransferIssuesModal(true)} />} - - +
+ +
- - - )} - - + {cycleId && } +
+ + )} + ); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 4ce0d53d5..762f1e281 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,134 +1,73 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import Link from "next/link"; import useSWR from "swr"; -// services -import { ModuleService } from "services/module.service"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; +import useLocalStorage from "hooks/use-local-storage"; // layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; +import { AppLayout } from "layouts/app-layout"; // components import { ExistingIssuesListModal } from "components/core"; import { ModuleDetailsSidebar } from "components/modules"; import { ModuleLayoutRoot } from "components/issues"; import { ModuleIssuesHeader } from "components/headers"; // ui -import { BreadcrumbItem, Breadcrumbs, CustomMenu, DiceIcon } from "@plane/ui"; import { EmptyState } from "components/common"; -// images +// assets import emptyModule from "public/empty-state/module.svg"; -// helpers -import { truncateText } from "helpers/string.helper"; -// types -import { ISearchIssueResponse } from "types"; -// fetch-keys -import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; - -// services -const moduleService = new ModuleService(); const SingleModule: React.FC = () => { const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - const [moduleSidebar, setModuleSidebar] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { user } = useUserAuth(); + const { module: moduleStore } = useMobxStore(); - const { setToastAlert } = useToast(); + const { storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); + const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; - const { data: modules } = useSWR( - workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, - workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null - ); - - const { data: moduleIssues } = useSWR( - workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null, + const { error } = useSWR( + workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null, workspaceSlug && projectId && moduleId - ? () => moduleService.getModuleIssues(workspaceSlug as string, projectId as string, moduleId as string) + ? () => moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()) : null ); - const { data: moduleDetails, error } = useSWR( - moduleId ? MODULE_DETAILS(moduleId as string) : null, - workspaceSlug && projectId - ? () => moduleService.getModuleDetails(workspaceSlug as string, projectId as string, moduleId as string) - : null - ); + // TODO: add this function to bulk add issues to cycle + // const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { + // if (!workspaceSlug || !projectId) return; - const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; + // const payload = { + // issues: data.map((i) => i.id), + // }; - const payload = { - issues: data.map((i) => i.id), - }; + // await moduleService + // .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) + // .catch(() => + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Selected issues could not be added to the module. Please try again.", + // }) + // ); + // }; - await moduleService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); - }; - - const openIssuesListModal = () => { - setModuleIssuesListModal(true); - }; + // const openIssuesListModal = () => { + // setModuleIssuesListModal(true); + // }; return ( <> - setModuleIssuesListModal(false)} - searchParams={{ module: true }} - handleOnSubmit={handleAddIssuesToModule} - /> - router.back()}> - - -

{`${truncateText( - moduleDetails?.project_detail.name ?? "Project", - 32 - )} Modules`}

-
- - } - /> - - } - left={ - - - {moduleDetails?.name && truncateText(moduleDetails.name, 40)} - - } - className="ml-1.5" - width="auto" - > - {modules?.map((module) => ( - router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)} - > - {truncateText(module.name, 40)} - - ))} - - } - right={} - > + } withProjectWrapper> + {/* TODO: Update logic to bulk add issues to a cycle */} + setModuleIssuesListModal(false)} + searchParams={{ module: true }} + handleOnSubmit={async () => {}} + /> {error ? ( { }} /> ) : ( - <> -
+
+
- - + {moduleId && } +
)} - + ); }; diff --git a/web/store/cycle/cycle_issue.store.ts b/web/store/cycle/cycle_issue.store.ts index 1e0092e42..ce484c0bb 100644 --- a/web/store/cycle/cycle_issue.store.ts +++ b/web/store/cycle/cycle_issue.store.ts @@ -5,6 +5,7 @@ import { RootStore } from "../root"; import { IIssue } from "types"; // services import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; // constants import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; @@ -34,6 +35,9 @@ export interface ICycleIssueStore { // action fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => void; + removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => void; } export class CycleIssueStore implements ICycleIssueStore { @@ -52,9 +56,11 @@ export class CycleIssueStore implements ICycleIssueStore { ungrouped: IIssue[]; }; } = {}; - // service - cycleService; + + // services rootStore; + cycleService; + issueService; constructor(_rootStore: RootStore) { makeObservable(this, { @@ -68,10 +74,14 @@ export class CycleIssueStore implements ICycleIssueStore { // actions fetchIssues: action, updateIssueStructure: action, + deleteIssue: action, + addIssueToCycle: action, + removeIssueFromCycle: action, }); this.rootStore = _rootStore; this.cycleService = new CycleService(); + this.issueService = new IssueService(); autorun(() => { const workspaceSlug = this.rootStore.workspace.workspaceSlug; @@ -130,7 +140,7 @@ export class CycleIssueStore implements ICycleIssueStore { issues = issues as IIssueGroupedStructure; issues = { ...issues, - [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + [group_id]: issues[group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)), }; } if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { @@ -139,27 +149,55 @@ export class CycleIssueStore implements ICycleIssueStore { ...issues, [sub_group_id]: { ...issues[sub_group_id], - [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + [group_id]: issues[sub_group_id][group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)), }, }; } if (issueType === "ungrouped") { issues = issues as IIssueUnGroupedStructure; - issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + issues = issues.map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)); } const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; - if (orderBy === "-created_at") { - issues = sortArrayByDate(issues as any, "created_at"); + if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "created_at"); + if (orderBy === "-updated_at") issues = sortArrayByDate(issues as any, "updated_at"); + if (orderBy === "start_date") issues = sortArrayByDate(issues as any, "updated_at"); + if (orderBy === "priority") issues = sortArrayByPriority(issues as any, "priority"); + + runInAction(() => { + this.issues = { ...this.issues, [cycleId]: { ...this.issues[cycleId], [issueType]: issues } }; + }); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const cycleId: string | null = this.rootStore.cycle.cycleId; + const issueType = this.getIssueType; + if (!cycleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; } - if (orderBy === "-updated_at") { - issues = sortArrayByDate(issues as any, "updated_at"); + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; } - if (orderBy === "start_date") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "priority") { - issues = sortArrayByPriority(issues as any, "priority"); + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); } runInAction(() => { @@ -199,4 +237,44 @@ export class CycleIssueStore implements ICycleIssueStore { return error; } }; + + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + try { + const user = this.rootStore.user.currentUser ?? undefined; + + await this.issueService.addIssueToCycle( + workspaceSlug, + projectId, + cycleId, + { + issues: [issueId], + }, + user + ); + + this.fetchIssues(workspaceSlug, projectId, cycleId); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => { + try { + await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, bridgeId); + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; } diff --git a/web/store/cycle/cycle_issue_kanban_view.store.ts b/web/store/cycle/cycle_issue_kanban_view.store.ts index 76e6a80e8..0ffbd883b 100644 --- a/web/store/cycle/cycle_issue_kanban_view.store.ts +++ b/web/store/cycle/cycle_issue_kanban_view.store.ts @@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" // types import { RootStore } from "../root"; import { IIssueType } from "store/issue"; -import { IUser } from "types"; export interface ICycleIssueKanBanViewStore { kanBanToggle: { @@ -293,8 +292,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - {} as IUser + updateIssue ); } }; @@ -442,8 +440,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - {} as IUser + updateIssue ); } }; diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 37a971b97..5b29fe9e2 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -33,6 +33,7 @@ export interface IIssueStore { // action fetchIssues: (workspaceSlug: string, projectId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; } export class IssueStore implements IIssueStore { @@ -67,6 +68,7 @@ export class IssueStore implements IIssueStore { // actions fetchIssues: action, updateIssueStructure: action, + deleteIssue: action, }); this.rootStore = _rootStore; @@ -163,6 +165,42 @@ export class IssueStore implements IIssueStore { }); }; + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + fetchIssues = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; diff --git a/web/store/issue/issue_detail.store.ts b/web/store/issue/issue_detail.store.ts index 6675ba26a..6b6c27eda 100644 --- a/web/store/issue/issue_detail.store.ts +++ b/web/store/issue/issue_detail.store.ts @@ -3,7 +3,7 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx" import { IssueService, IssueReactionService } from "services/issue"; // types import { RootStore } from "../root"; -import { IUser, IIssue } from "types"; +import { IIssue } from "types"; // constants import { groupReactionEmojis } from "constants/issue"; @@ -36,17 +36,11 @@ export interface IIssueDetailStore { // fetch issue details fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; // creating issue - createIssue: (workspaceSlug: string, projectId: string, data: Partial, user: IUser) => Promise; + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; // updating issue - updateIssue: ( - workspaceId: string, - projectId: string, - issueId: string, - data: Partial, - user: IUser | undefined - ) => void; + updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial) => void; // deleting issue - deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void; + deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -210,13 +204,15 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - createIssue = async (workspaceSlug: string, projectId: string, data: Partial, user: IUser) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { try { runInAction(() => { this.loader = true; this.error = null; }); + const user = this.rootStore.user.currentUser ?? undefined; + const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user); runInAction(() => { @@ -237,13 +233,7 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - updateIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - user: IUser | undefined - ) => { + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { const newIssues = { ...this.issues }; newIssues[issueId] = { ...newIssues[issueId], @@ -257,6 +247,10 @@ export class IssueDetailStore implements IIssueDetailStore { this.issues = newIssues; }); + const user = this.rootStore.user.currentUser; + + if (!user) return; + const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user); runInAction(() => { @@ -282,7 +276,7 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => { + deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { const newIssues = { ...this.issues }; delete newIssues[issueId]; @@ -293,12 +287,18 @@ export class IssueDetailStore implements IIssueDetailStore { this.issues = newIssues; }); - await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user); + const user = this.rootStore.user.currentUser; + + if (!user) return; + + const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user); runInAction(() => { this.loader = false; this.error = null; }); + + return response; } catch (error) { this.fetchIssueDetails(workspaceSlug, projectId, issueId); diff --git a/web/store/issue/issue_filters.store.ts b/web/store/issue/issue_filters.store.ts index 91743de29..eba03ce86 100644 --- a/web/store/issue/issue_filters.store.ts +++ b/web/store/issue/issue_filters.store.ts @@ -123,7 +123,7 @@ export class IssueFilterStore implements IIssueFilterStore { labels: this.userFilters?.labels || undefined, start_date: this.userFilters?.start_date || undefined, target_date: this.userFilters?.target_date || undefined, - group_by: this.userDisplayFilters?.group_by || "state", + group_by: this.userDisplayFilters?.group_by || undefined, order_by: this.userDisplayFilters?.order_by || "-created_at", sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, type: this.userDisplayFilters?.type || undefined, diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index d1f07352f..9faf3190c 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" // types import { RootStore } from "../root"; import { IIssueType } from "./issue.store"; -import { IUser } from "types"; export interface IIssueKanBanViewStore { kanBanToggle: { @@ -293,8 +292,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - {} as IUser + updateIssue ); } }; @@ -442,8 +440,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - {} as IUser + updateIssue ); } }; diff --git a/web/store/module/module_issue.store.ts b/web/store/module/module_issue.store.ts index 8b361b1fd..5cd794861 100644 --- a/web/store/module/module_issue.store.ts +++ b/web/store/module/module_issue.store.ts @@ -34,6 +34,9 @@ export interface IModuleIssueStore { // action fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; + removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise; } export class ModuleIssueStore implements IModuleIssueStore { @@ -52,9 +55,10 @@ export class ModuleIssueStore implements IModuleIssueStore { ungrouped: IIssue[]; }; } = {}; - // service - moduleService; + + // services rootStore; + moduleService; constructor(_rootStore: RootStore) { makeObservable(this, { @@ -68,6 +72,9 @@ export class ModuleIssueStore implements IModuleIssueStore { // actions fetchIssues: action, updateIssueStructure: action, + deleteIssue: action, + addIssueToModule: action, + removeIssueFromModule: action, }); this.rootStore = _rootStore; @@ -167,6 +174,42 @@ export class ModuleIssueStore implements IModuleIssueStore { }); }; + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const moduleId: string | null = this.rootStore.module.moduleId; + const issueType = this.getIssueType; + if (!moduleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } }; + }); + }; + fetchIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { try { this.loader = true; @@ -204,4 +247,44 @@ export class ModuleIssueStore implements IModuleIssueStore { return error; } }; + + addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { + try { + const user = this.rootStore.user.currentUser ?? undefined; + + await this.moduleService.addIssuesToModule( + workspaceSlug, + projectId, + moduleId, + { + issues: [issueId], + }, + user + ); + + this.fetchIssues(workspaceSlug, projectId, moduleId); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => { + try { + await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, bridgeId); + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; } diff --git a/web/store/module/module_issue_kanban_view.store.ts b/web/store/module/module_issue_kanban_view.store.ts index 7061316f3..d06fbf02b 100644 --- a/web/store/module/module_issue_kanban_view.store.ts +++ b/web/store/module/module_issue_kanban_view.store.ts @@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" // types import { RootStore } from "../root"; import { IIssueType } from "../issue/issue.store"; -import { IUser } from "types"; export interface IModuleIssueKanBanViewStore { kanBanToggle: { @@ -293,8 +292,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - this.rootStore.user.currentUser as IUser + updateIssue ); } }; @@ -442,8 +440,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { updateIssue.workspaceSlug, updateIssue.projectId, updateIssue.issueId, - updateIssue, - this.rootStore.user.currentUser as IUser + updateIssue ); } }; diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index f73b5952f..da641ece9 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -38,7 +38,7 @@ export interface IModuleStore { getModuleById: (moduleId: string) => IModule | null; fetchModules: (workspaceSlug: string, projectId: string) => void; - fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => void; + fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => void; @@ -171,8 +171,6 @@ export class ModuleStore implements IModuleStore { const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); - if (!response) return null; - runInAction(() => { this.moduleDetails = { ...this.moduleDetails, @@ -181,6 +179,8 @@ export class ModuleStore implements IModuleStore { this.loader = false; this.error = null; }); + + return response; } catch (error) { console.error("Failed to fetch module details in module store", error); @@ -188,6 +188,8 @@ export class ModuleStore implements IModuleStore { this.loader = false; this.error = error; }); + + throw error; } }; diff --git a/web/store/profile-issues/issue.store.ts b/web/store/profile-issues/issue.store.ts index 75898c1b4..502fdcc52 100644 --- a/web/store/profile-issues/issue.store.ts +++ b/web/store/profile-issues/issue.store.ts @@ -36,6 +36,7 @@ export interface IProfileIssueStore { // action fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; } export class ProfileIssueStore implements IProfileIssueStore { @@ -76,6 +77,7 @@ export class ProfileIssueStore implements IProfileIssueStore { // actions fetchIssues: action, updateIssueStructure: action, + deleteIssue: action, }); this.rootStore = _rootStore; this.userService = new UserService(); @@ -174,6 +176,55 @@ export class ProfileIssueStore implements IProfileIssueStore { }); }; + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + + const issueType = this.getIssueType; + + if (!workspaceSlug || !userId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i: IIssue) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i: IIssue) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i: IIssue) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issues, + }, + }, + }; + }); + }; + fetchIssues = async ( workspaceSlug: string, userId: string, diff --git a/web/store/project-view/project_view_issues.store.ts b/web/store/project-view/project_view_issues.store.ts index 11571b8da..8022204cf 100644 --- a/web/store/project-view/project_view_issues.store.ts +++ b/web/store/project-view/project_view_issues.store.ts @@ -30,6 +30,7 @@ export interface IProjectViewIssuesStore { // actions updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; fetchViewIssues: ( workspaceSlug: string, projectId: string, @@ -72,6 +73,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore { // actions updateIssueStructure: action, + deleteIssue: action, fetchViewIssues: action, // computed @@ -167,6 +169,42 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore { }); }; + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const viewId: string | null = this.rootStore.projectViews.viewId; + const issueType = this.rootStore.issue.getIssueType; + if (!viewId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.viewIssues = { ...this.viewIssues, [viewId]: { ...this.viewIssues[viewId], [issueType]: issues } }; + }); + }; + fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => { try { runInAction(() => {