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
This commit is contained in:
Aaryan Khandelwal 2023-10-20 17:07:46 +05:30 committed by GitHub
parent 9bddd2eb67
commit d78b4dccf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2336 additions and 1153 deletions

View File

@ -323,6 +323,10 @@ module.exports = {
"0%": { right: "-20rem" },
"100%": { right: "0" },
},
"bar-loader": {
from: { left: "-100%" },
to: { left: "100%" },
},
},
typography: ({ theme }) => ({
brand: {

View File

@ -35,7 +35,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
React.useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
placement: placement ?? "auto",
});
return (
<Menu as="div" className={`relative w-min text-left ${className}`}>
@ -100,9 +100,9 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
)}
</>
)}
<Menu.Items>
<Menu.Items className="fixed z-10">
<div
className={`z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-custom-shadow-rg focus:outline-none bg-custom-background-90 my-1 ${
className={`overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-custom-shadow-rg focus:outline-none bg-custom-background-90 my-1 ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"

View File

@ -223,7 +223,6 @@ export const CommandPalette: FC = observer(() => {
handleClose={() => toggleDeleteIssueModal(false)}
isOpen={isDeleteIssueModalOpen}
data={issueDetails}
user={user}
/>
)}

View File

@ -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<Props> = ({ cycle, isOpen, cycleStatus, isCompleted, user }) => {
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 && (
<CycleDeleteModal
cycle={cycle}
cycle={cycleDetails}
modal={cycleDeleteModal}
modalClose={() => setCycleDeleteModal(false)}
onSubmit={() => {}}
workspaceSlug={workspaceSlug}
projectId={projectId}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
<div
className={`fixed top-[66px] z-20 ${
className={`absolute top-0 z-20 ${
isOpen ? "right-0" : "-right-[24rem]"
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-200 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
>
{cycle ? (
{cycleDetails ? (
<>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm">
@ -296,13 +313,13 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycle.start_date ? "" : "text-custom-text-200"
cycleDetails.start_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3" />
<span>
{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"
)}
</span>
@ -319,7 +336,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycle?.start_date}
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
@ -344,14 +361,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycle.end_date ? "" : "text-custom-text-200"
cycleDetails.end_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3" />
<span>
{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"
)}
</span>
@ -368,7 +385,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycle?.end_date}
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
@ -391,7 +408,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2">
<div className="max-w-[300px]">
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">{cycle.name}</h4>
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">
{cycleDetails.name}
</h4>
</div>
<CustomMenu width="lg" ellipsis>
{!isCompleted && (
@ -412,7 +431,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div>
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
{cycle.description}
{cycleDetails.description}
</span>
</div>
@ -424,20 +443,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div>
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
{cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
src={cycleDetails.owned_by.avatar}
height={12}
width={12}
className="rounded-full"
alt={cycle.owned_by.display_name}
alt={cycleDetails.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
{cycleDetails.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
<span className="text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
</div>
</div>
@ -449,9 +468,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<div className="flex items-center gap-2.5 text-custom-text-200">
<span className="h-4 w-4">
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} />
<ProgressBar value={cycleDetails.completed_issues} maxValue={cycleDetails.total_issues} />
</span>
{cycle.completed_issues}/{cycle.total_issues}
{cycleDetails.completed_issues}/{cycleDetails.total_issues}
</div>
</div>
</div>
@ -498,7 +517,8 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</span>
<span>
Pending Issues -{" "}
{cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}
{cycleDetails.total_issues -
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
</span>
</div>
@ -515,10 +535,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div>
<div className="relative">
<ProgressChart
distribution={cycle.distribution.completion_chart}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
distribution={cycleDetails.distribution.completion_chart}
startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""}
totalIssues={cycleDetails.total_issues}
/>
</div>
</div>
@ -540,7 +560,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<span className="font-medium text-custom-text-200">Other Information</span>
</div>
{cycle.total_issues > 0 ? (
{cycleDetails.total_issues > 0 ? (
<Disclosure.Button>
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button>
@ -555,18 +575,18 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div>
<Transition show={open}>
<Disclosure.Panel>
{cycle.total_issues > 0 ? (
{cycleDetails.total_issues > 0 ? (
<div className="h-full w-full py-4">
<SidebarProgressStats
distribution={cycle.distribution}
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycle.total_issues}
totalIssues={cycleDetails.total_issues}
/>
</div>
) : (
@ -595,4 +615,4 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div>
</>
);
};
});

View File

@ -19,7 +19,9 @@ type Props = {
const cycleService = new CycleService();
export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
export const TransferIssues: React.FC<Props> = (props) => {
const { handleClick } = props;
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
@ -33,8 +35,9 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
const transferableIssuesCount = cycleDetails
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
: 0;
return (
<div className="-mt-2 mb-4 flex items-center justify-between px-8 pt-6">
<div className="-mt-2 mb-4 flex items-center justify-between px-4 pt-6">
<div className="flex items-center gap-2 text-sm text-custom-text-200">
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
<span>Completed cycles are not editable.</span>

View File

@ -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,6 +108,39 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
cycleDetails={cycleDetails ?? undefined}
/>
<div className="relative w-full flex items-center z-10 justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
link={
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}>
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<p className="truncate">{`${truncateText(cycleDetails?.project_detail.name ?? "", 32)} Cycles`}</p>
</a>
</Link>
}
/>
</Breadcrumbs>
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
{cycleDetails?.name && truncateText(cycleDetails.name, 40)}
</>
}
className="ml-1.5 flex-shrink-0"
width="auto"
>
{cyclesList?.map((cycle) => (
<CustomMenu.MenuItem
key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
>
{truncateText(cycle.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
@ -141,6 +185,14 @@ export const CycleIssuesHeader: React.FC = observer(() => {
>
Add Issue
</Button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar}
>
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
</button>
</div>
</div>
</>
);

View File

@ -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,6 +108,42 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined}
/>
<div className="relative w-full flex items-center z-10 justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
link={
<Link href={`/${workspaceSlug}/projects/${projectId}/modules`}>
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<p className="truncate">{`${truncateText(
moduleDetails?.project_detail.name ?? "",
32
)} Modules`}</p>
</a>
</Link>
}
/>
</Breadcrumbs>
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
className="ml-1.5 flex-shrink-0"
width="auto"
>
{modulesList?.map((module) => (
<CustomMenu.MenuItem
key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
>
{truncateText(module.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
@ -141,6 +188,14 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
>
Add Issue
</Button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar}
>
<ArrowRight className={`h-4 w-4 duration-300 ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
</button>
</div>
</div>
</>
);

View File

@ -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<void>;
redirection?: boolean;
};
const issueService = new IssueService();
const issueArchiveService = new IssueArchiveService();
export const DeleteIssueModal: React.FC<Props> = ({
isOpen,
handleClose,
data,
user,
onSubmit,
redirection = true,
}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
export const DeleteIssueModal: React.FC<Props> = 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,76 +36,15 @@ export const DeleteIssueModal: React.FC<Props> = ({
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<ISubIssueResponse>(
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,
if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false));
};
},
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();
};
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 (
<Transition.Root show={isOpen} as={Fragment}>
@ -194,4 +108,4 @@ export const DeleteIssueModal: React.FC<Props> = ({
</Dialog>
</Transition.Root>
);
};
});

View File

@ -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<Props> = 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<Props> = observer((props) => {
<>
<div className="h-full w-full flex flex-col overflow-hidden">
<CalendarHeader />
<CalendarWeekHeader />
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
<div className="h-full w-full overflow-y-auto">
{layout === "month" ? (
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
{allWeeksOfActiveMonth &&
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
<CalendarWeekDays key={weekIndex} week={week} issues={issues} />
<CalendarWeekDays key={weekIndex} week={week} issues={issues} quickActions={quickActions} />
))}
</div>
) : (
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} />
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} quickActions={quickActions} />
)}
</div>
</div>

View File

@ -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<Props> = observer((props) => {
const { date, issues } = props;
const { date, issues, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore();
@ -48,7 +53,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
{date.date.getDate()}
</div>
<CalendarIssueBlocks issues={issuesList} />
<CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
{provided.placeholder}
</>
</div>

View File

@ -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<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(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 (
<Popover className="relative">
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className="outline-none text-xl font-semibold"
disabled={calendarLayout === "week"}
>
{calendarLayout === "month"
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
: getWeekLayoutHeader()}
</button>
</Popover.Button>
<Transition
as={React.Fragment}
@ -61,8 +83,13 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div className="absolute left-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200">
<Popover.Panel className="fixed z-50">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200"
>
<div className="flex items-center justify-between gap-2 pb-3">
<button
type="button"

View File

@ -1,8 +1,8 @@
import React from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
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";
// ui
@ -20,6 +20,21 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
const { issueFilter: issueFilterStore, calendar: calendarStore } = useMobxStore();
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
@ -57,12 +72,12 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
return (
<Popover className="relative">
{({ open }) => {
if (open) {
}
return (
{({ open }) => (
<>
<Popover.Button
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={`outline-none bg-custom-background-80 text-xs rounded flex items-center gap-1.5 px-2.5 py-1 hover:bg-custom-background-80 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
@ -73,6 +88,7 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
>
<ChevronUp width={12} strokeWidth={2} />
</div>
</button>
</Popover.Button>
<Transition
as={React.Fragment}
@ -83,8 +99,13 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded min-w-[12rem] p-1 overflow-hidden">
<Popover.Panel className="fixed z-50">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded min-w-[12rem] p-1 overflow-hidden"
>
<div>
{Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => (
<button
@ -110,8 +131,7 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
</Popover.Panel>
</Transition>
</>
);
}}
)}
</Popover>
);
});

View File

@ -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<Props> = 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<Props> = observer((props) => {
{(provided, snapshot) => (
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
<a
className={`h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
snapshot.isDragging
? "shadow-custom-shadow-rg bg-custom-background-90"
: "bg-custom-background-100 hover:bg-custom-background-90"
@ -40,6 +42,12 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
{/* <IssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/> */}
</a>
</Link>
)}

View File

@ -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 (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromCycle={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/>
</DragDropContext>
</div>

View File

@ -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 (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromModule={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/>
</DragDropContext>
</div>

View File

@ -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 (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/>
</DragDropContext>
</div>

View File

@ -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 (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/>
</DragDropContext>
</div>

View File

@ -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<Props> = observer((props) => {
const { issues, week } = props;
const { issues, week, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore();
@ -34,7 +36,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
{Object.values(week).map((date: ICalendarDate) => {
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
return <CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} />;
return (
<CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} quickActions={quickActions} />
);
})}
</div>
);

View File

@ -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<Props> = observer((props) => {
const { isLoading, showWeekends } = props;
return (
<div
className={`grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
className={`relative grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
showWeekends ? "grid-cols-7" : "grid-cols-5"
}`}
>
{isLoading && (
<div className="absolute h-[1.5px] w-3/4 bg-custom-primary-100 animate-[bar-loader_2s_linear_infinite]" />
)}
{Object.values(DAYS_LIST).map((day) => {
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;

View File

@ -111,8 +111,8 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
<div className="py-2">
<FilterExtraOptions
selectedExtraOptions={{
show_empty_groups: displayFilters.show_empty_groups ?? false,
sub_issue: displayFilters.sub_issue ?? false,
show_empty_groups: displayFilters.show_empty_groups ?? true,
sub_issue: displayFilters.sub_issue ?? true,
}}
handleUpdate={(key, val) =>
handleDisplayFiltersUpdate({

View File

@ -20,6 +20,8 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const activeGroupBy = selectedGroupBy ?? null;
return (
<>
<FilterHeader
@ -35,7 +37,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
return (
<FilterOption
key={groupBy?.key}
isChecked={selectedGroupBy === groupBy?.key ? true : false}
isChecked={activeGroupBy === groupBy?.key ? true : false}
onClick={() => handleUpdate(groupBy.key)}
title={groupBy.title}
multiple={false}

View File

@ -18,6 +18,8 @@ export const FilterIssueType: React.FC<Props> = observer((props) => {
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const activeIssueType = selectedIssueType ?? null;
return (
<>
<FilterHeader
@ -30,7 +32,7 @@ export const FilterIssueType: React.FC<Props> = observer((props) => {
{ISSUE_FILTER_OPTIONS.map((issueType) => (
<FilterOption
key={issueType?.key}
isChecked={selectedIssueType === issueType?.key ? true : false}
isChecked={activeIssueType === issueType?.key ? true : false}
onClick={() => handleUpdate(issueType?.key)}
title={issueType.title}
multiple={false}

View File

@ -19,6 +19,8 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const activeOrderBy = selectedOrderBy ?? "-created_at";
return (
<>
<FilterHeader
@ -31,7 +33,7 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
{ISSUE_ORDER_BY_OPTIONS.filter((option) => orderByOptions.includes(option.key)).map((orderBy) => (
<FilterOption
key={orderBy?.key}
isChecked={selectedOrderBy === orderBy?.key ? true : false}
isChecked={activeOrderBy === orderBy?.key ? true : false}
onClick={() => handleUpdate(orderBy.key)}
title={orderBy.title}
multiple={false}

View File

@ -1,5 +1,6 @@
// filters
export * from "./filters";
export * from "./quick-action-dropdowns";
// layouts
export * from "./list";

View File

@ -1,50 +1,58 @@
// 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) => (
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (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 (
<>
{issues && issues.length > 0 ? (
<>
{issues.map((issue: any, index: any) => (
<Draggable
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
draggableId={`${issue.id}`}
index={index}
isDragDisabled={isDragDisabled}
>
{(provided: any, snapshot: any) => (
<Draggable draggableId={issue.id} index={index} isDragDisabled={isDragDisabled}>
{(provided, snapshot) => (
<div
key={issue.id}
className="p-1.5 hover:cursor-default"
className="group/kanban-block relative p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<div className="absolute top-3 right-3 hidden group-hover/kanban-block:block">
{quickActions(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!columnId && columnId === "null" ? null : columnId,
issue
)}
</div>
<div
className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
}`}
>
{display_properties && display_properties?.key && (
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
{displayProperties && displayProperties?.key && (
<div className="text-xs line-clamp-1 text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
<div>
@ -52,22 +60,14 @@ export const IssueBlock = ({
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
handleIssues={handleIssues}
display_properties={display_properties}
handleIssues={updateIssue}
display_properties={displayProperties}
/>
</div>
</div>
</div>
)}
</Draggable>
))}
</>
) : (
!isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
</>
);
);
};

View File

@ -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<IssueBlocksListProps> = (props) => {
const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, display_properties } = props;
return (
<>
{issues && issues.length > 0 ? (
<>
{issues.map((issue, index) => (
<KanbanIssueBlock
key={`kanban-issue-block-${issue.id}`}
index={index}
issue={issue}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={display_properties}
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
/>
))}
</>
) : (
!isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
</>
);
};

View File

@ -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) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => 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) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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<IGroupByKanBan> = observer(
({
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const {
issues,
sub_group_by,
group_by,
@ -36,10 +41,12 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
listKey,
isDragDisabled,
handleIssues,
quickActions,
display_properties,
kanBanToggle,
handleKanBanToggle,
}) => {
} = props;
const verticalAlignPosition = (_list: any) =>
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
@ -78,12 +85,13 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
ref={provided.innerRef}
>
{issues ? (
<IssueBlock
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={getValueFromObject(_list, listKey) as string}
issues={issues[getValueFromObject(_list, listKey) as string]}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
/>
) : (
@ -102,8 +110,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
))}
</div>
);
}
);
});
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<IKanBan> = observer((props) => {
group_by,
sub_group_id = "null",
handleIssues,
quickActions,
display_properties,
kanBanToggle,
handleKanBanToggle,
@ -144,7 +158,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
estimates,
} = props;
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore();
return (
<div className="relative w-full h-full">
@ -158,6 +172,7 @@ export const KanBan: React.FC<IKanBan> = 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<IKanBan> = 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<IKanBan> = 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<IKanBan> = 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<IKanBan> = 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<IKanBan> = observer((props) => {
listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}

View File

@ -1,3 +1,5 @@
export * from "./block";
export * from "./blocks-list";
export * from "./cycle-root";
export * from "./module-root";
export * from "./root";

View File

@ -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) => {
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) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => 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) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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) => {
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) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => 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) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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) => {
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) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => 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) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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<ISubGroupSwimlaneHeader> = ({
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<ISubGroupSwimlane> = observer((props) => {
list,
listKey,
handleIssues,
quickActions,
display_properties,
kanBanToggle,
handleKanBanToggle,
@ -130,6 +137,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = 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<IKanBanSwimLanes> = observer((props) => {
sub_group_by,
group_by,
handleIssues,
quickActions,
display_properties,
kanBanToggle,
handleKanBanToggle,
@ -184,7 +199,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
estimates,
} = props;
const { project: projectStore }: RootStore = useMobxStore();
const { project: projectStore } = useMobxStore();
return (
<div className="relative">
@ -270,6 +285,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = observer((props) => {
list={projectStore?.projectMembers}
listKey={`member.id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}

View File

@ -62,45 +62,47 @@ export const ViewKanBanLayout: React.FC = observer(() => {
const projects = projectStore?.projectStates || null;
const estimates = null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={updateIssue}
display_properties={display_properties}
kanBanToggle={() => {}}
handleKanBanToggle={() => {}}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
estimates={estimates}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={updateIssue}
display_properties={display_properties}
kanBanToggle={() => {}}
handleKanBanToggle={() => {}}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
estimates={estimates}
/>
)}
</DragDropContext>
</div>
);
return null;
// return (
// <div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
// <DragDropContext onDragEnd={onDragEnd}>
// {currentKanBanView === "default" ? (
// <KanBan
// issues={issues}
// sub_group_by={sub_group_by}
// group_by={group_by}
// handleIssues={updateIssue}
// display_properties={display_properties}
// kanBanToggle={() => {}}
// handleKanBanToggle={() => {}}
// states={states}
// stateGroups={stateGroups}
// priorities={priorities}
// labels={labels}
// members={members}
// projects={projects}
// estimates={estimates}
// />
// ) : (
// <KanBanSwimLanes
// issues={issues}
// sub_group_by={sub_group_by}
// group_by={group_by}
// handleIssues={updateIssue}
// display_properties={display_properties}
// kanBanToggle={() => {}}
// handleKanBanToggle={() => {}}
// states={states}
// stateGroups={stateGroups}
// priorities={priorities}
// labels={labels}
// members={members}
// projects={projects}
// estimates={estimates}
// />
// )}
// </DragDropContext>
// </div>
// );
});

View File

@ -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<IssueBlockProps> = (props) => {
const { columnId, issues, handleIssues, display_properties, states, labels, members, priorities } = props;
export const IssueBlock: React.FC<IssueBlockProps> = (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) => (
<div
key={index}
className={`text-sm p-3 shadow-custom-shadow-2xs bg-custom-background-100 flex items-center gap-3 border-b border-custom-border-200 hover:bg-custom-background-80`}
>
<div className="text-sm p-3 shadow-custom-shadow-2xs bg-custom-background-100 flex items-center gap-3 border-b border-custom-border-200 hover:bg-custom-background-80">
{display_properties && display_properties?.key && (
<div className="flex-shrink-0 text-xs text-custom-text-300">
{issue?.project_detail?.identifier}-{issue.sequence_id}
</div>
)}
<IssuePeekOverview
workspaceSlug={issue?.workspace_detail?.slug}
projectId={issue?.project_detail?.id}
issueId={issue?.id}
handleIssue={handleIssue}
// TODO: add the logic here
handleIssue={() => {}}
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full">{issue.name}</div>
</Tooltip>
</IssuePeekOverview>
<div className="ml-auto flex-shrink-0">
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
<KanBanProperties
columnId={columnId}
issue={issue}
handleIssues={handleIssues}
handleIssues={updateIssue}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
priorities={priorities}
/>
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
</div>
</div>
))}
</>
);
};

View File

@ -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> = (props) => {
const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, priorities } =
props;
return (
<>
{issues &&
issues?.length > 0 &&
issues.map((issue) => (
<IssueBlock
key={issue.id}
columnId={columnId}
issue={issue}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
priorities={priorities}
/>
))}
</>
);
};

View File

@ -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) => {
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(() => {
<List
issues={issues}
group_by={group_by}
handleIssues={updateIssue}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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<IGroupByList> = observer((props) => {
list,
listKey,
handleIssues,
quickActions,
display_properties,
is_list = false,
states,
@ -59,10 +63,11 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
</div>
<div className={`w-full h-full relative transition-all`}>
{issues && (
<IssueBlock
<IssueBlocksList
columnId={getValueFromObject(_list, listKey) as string}
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
@ -77,11 +82,13 @@ const GroupByList: React.FC<IGroupByList> = 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<IList> = observer((props) => {
issues,
group_by,
handleIssues,
quickActions,
display_properties,
states,
labels,
@ -116,6 +124,7 @@ export const List: React.FC<IList> = 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<IList> = 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<IList> = 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<IList> = 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<IList> = 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<IList> = 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<IList> = 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<IList> = observer((props) => {
list={members}
listKey={`member.id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}

View File

@ -1,3 +1,5 @@
export * from "./block";
export * from "./blocks-list";
export * from "./cycle-root";
export * from "./module-root";
export * from "./root";

View File

@ -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) => {
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(() => {
<List
issues={issues}
group_by={group_by}
handleIssues={updateIssue}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => 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}

View File

@ -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(() => {
<List
issues={issues}
group_by={group_by}
handleIssues={updateIssue}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
display_properties={display_properties}
states={states}
stateGroups={stateGroups}

View File

@ -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;

View File

@ -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) => {
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 (
<div className={`relative w-full h-full bg-custom-background-90`}>
<div className="relative w-full h-full bg-custom-background-90">
<List
issues={issues}
group_by={group_by}
handleIssues={updateIssue}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
display_properties={display_properties}
states={states}
stateGroups={stateGroups}

View File

@ -31,21 +31,23 @@ export const ViewListLayout: React.FC = observer(() => {
const projects = projectStore?.projectStates || null;
const estimates = null;
return (
<div className={`relative w-full h-full bg-custom-background-90`}>
<List
issues={issues}
group_by={group_by}
handleIssues={updateIssue}
display_properties={display_properties}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
estimates={estimates}
/>
</div>
);
return null;
// return (
// <div className={`relative w-full h-full bg-custom-background-90`}>
// <List
// issues={issues}
// group_by={group_by}
// handleIssues={updateIssue}
// display_properties={display_properties}
// states={states}
// stateGroups={stateGroups}
// priorities={priorities}
// labels={labels}
// members={members}
// projects={projects}
// estimates={estimates}
// />
// </div>
// );
});

View File

@ -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<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromCycle: () => Promise<void>;
};
export const CycleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(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 (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
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 });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromCycle();
}}
>
<div className="flex items-center gap-2">
<XCircle className="h-3 w-3" />
Remove from cycle
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

@ -0,0 +1,3 @@
export * from "./cycle-issue";
export * from "./module-issue";
export * from "./project-issue";

View File

@ -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<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromModule: () => Promise<void>;
};
export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(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 (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
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 });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromModule();
}}
>
<div className="flex items-center gap-2">
<XCircle className="h-3 w-3" />
Remove from module
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

@ -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<void>;
handleUpdate: (data: IIssue) => Promise<void>;
};
export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(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 (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
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 });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

@ -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<Props> = ({
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div className="flex flex-col whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none min-w-full bg-custom-background-100 space-y-0.5">
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
<span>Copy link</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
@ -98,7 +112,7 @@ export const IssueColumn: React.FC<Props> = ({
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
className="w-full select-none gap-2 rounded p-1 text-left text-red-500 hover:bg-custom-background-80"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
@ -109,20 +123,6 @@ export const IssueColumn: React.FC<Props> = ({
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"

View File

@ -45,7 +45,7 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<>
<IssueColumn
issue={issue}
projectId={projectId}
@ -62,7 +62,7 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
subIssues.map((subIssue) => (
<SpreadsheetIssuesColumn
key={subIssue.id}
issue={subIssue}
@ -75,6 +75,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
nestingLevel={nestingLevel + 1}
/>
))}
</div>
</>
);
};

View File

@ -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<IIssue>) => {
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 (

View File

@ -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<IIssue>) => {
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 (

View File

@ -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]
);

View File

@ -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<IIssue>) => {
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 (

View File

@ -30,7 +30,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issueUpdate = (_data: Partial<IIssue>) => {
if (handleIssue) {
handleIssue(_data);
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data, undefined);
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data);
}
};

View File

@ -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<IIssue>;
fieldsToShow?: (
| "project"
@ -46,15 +45,7 @@ const issueService = new IssueService();
const issueDraftService = new IssueDraftService();
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = 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<IssuesModalProps> = observer((prop
};
const createIssue = async (payload: Partial<IIssue>) => {
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<IssuesModalProps> = observer((prop
await issueService
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(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({

View File

@ -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<Props> = () => {
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<Props> = () => {
mutateMyIssues();
}}
/>
{issueToDelete && (
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateMyIssues();
}}
/>
)}
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">

View File

@ -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<Props> = 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<Props> = observer(({ handleMutation, pr
};
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
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<Props> = observer(({ handleMutation, pr
return (
<>
{issue && (
<DeleteIssueModal
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
data={issue ? { ...issue } : null}
data={issue}
onSubmit={handleDeleteIssue}
user={user}
/>
)}
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">

View File

@ -277,12 +277,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail ?? null}
user={user}
/>
{issueDetail && (
<DeleteIssueModal handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetail} />
)}
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex items-center justify-between px-5 pb-3">
<h4 className="text-sm font-medium">

View File

@ -335,7 +335,10 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
/>
</>
)}
{isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && (
{isEditable &&
issueCrudOperation?.delete?.toggle &&
issueCrudOperation?.delete?.issueId &&
issueCrudOperation?.delete?.issue && (
<DeleteIssueModal
isOpen={issueCrudOperation?.delete?.toggle}
handleClose={() => {
@ -343,8 +346,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
handleIssueCrudOperation("delete", null, null);
}}
data={issueCrudOperation?.delete?.issue}
user={user}
redirection={false}
/>
)}
</>

View File

@ -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<IModule> = {
};
type Props = {
module?: IModule;
isOpen: boolean;
moduleIssues?: IIssue[];
user: IUser | undefined;
moduleId: string;
};
// services
const moduleService = new ModuleService();
export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => {
// TODO: refactor this component
export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { isOpen, moduleId } = props;
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(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<Props> = ({ 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<Props> = ({ 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<IModule>(
MODULE_DETAILS(module.id),
@ -165,41 +176,45 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ 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 (
<>
<LinkModal
@ -213,7 +228,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/>
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} user={user} />
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={moduleDetails} user={user} />
<div
className={`fixed top-[66px] ${
isOpen ? "right-0" : "-right-[24rem]"
@ -254,11 +269,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<>
<Popover.Button
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
module.start_date ? "" : "text-custom-text-200"
moduleDetails.start_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3" />
<span>{renderShortDateWithYearFormat(new Date(`${module.start_date}`), "Start date")}</span>
<span>
{renderShortDateWithYearFormat(new Date(`${moduleDetails.start_date}`), "Start date")}
</span>
</Popover.Button>
<Transition
@ -298,12 +315,14 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<>
<Popover.Button
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
module.target_date ? "" : "text-custom-text-200"
moduleDetails.target_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3 " />
<span>{renderShortDateWithYearFormat(new Date(`${module?.target_date}`), "End date")}</span>
<span>
{renderShortDateWithYearFormat(new Date(`${moduleDetails?.target_date}`), "End date")}
</span>
</Popover.Button>
<Transition
@ -342,7 +361,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2 ">
<div className="max-w-[300px]">
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{module.name}</h4>
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">
{moduleDetails.name}
</h4>
</div>
<CustomMenu width="lg" ellipsis>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
@ -361,7 +382,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div>
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
{module.description}
{moduleDetails.description}
</span>
</div>
@ -399,9 +420,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex items-center gap-2.5 text-custom-text-200">
<span className="h-4 w-4">
<ProgressBar value={module.completed_issues} maxValue={module.total_issues} />
<ProgressBar value={moduleDetails.completed_issues} maxValue={moduleDetails.total_issues} />
</span>
{module.completed_issues}/{module.total_issues}
{moduleDetails.completed_issues}/{moduleDetails.total_issues}
</div>
</div>
</div>
@ -415,7 +436,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-custom-text-200">Progress</span>
{!open && moduleIssues && progressPercentage ? (
{!open && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
@ -439,7 +460,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div>
<Transition show={open}>
<Disclosure.Panel>
{isStartValid && isEndValid && moduleIssues ? (
{isStartValid && isEndValid ? (
<div className=" h-full w-full py-4">
<div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1">
@ -448,7 +469,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</span>
<span>
Pending Issues -{" "}
{module.total_issues - (module.completed_issues + module.cancelled_issues)}{" "}
{moduleDetails.total_issues -
(moduleDetails.completed_issues + moduleDetails.cancelled_issues)}{" "}
</span>
</div>
@ -465,10 +487,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={module.distribution.completion_chart}
startDate={module.start_date ?? ""}
endDate={module.target_date ?? ""}
totalIssues={module.total_issues}
distribution={moduleDetails.distribution.completion_chart}
startDate={moduleDetails.start_date ?? ""}
endDate={moduleDetails.target_date ?? ""}
totalIssues={moduleDetails.total_issues}
/>
</div>
</div>
@ -491,7 +513,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<span className="font-medium text-custom-text-200">Other Information</span>
</div>
{module.total_issues > 0 ? (
{moduleDetails.total_issues > 0 ? (
<Disclosure.Button className="p-1">
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button>
@ -506,20 +528,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div>
<Transition show={open}>
<Disclosure.Panel>
{module.total_issues > 0 ? (
{moduleDetails.total_issues > 0 ? (
<>
<div className=" h-full w-full py-4">
<SidebarProgressStats
distribution={module.distribution}
distribution={moduleDetails.distribution}
groupedIssues={{
backlog: module.backlog_issues,
unstarted: module.unstarted_issues,
started: module.started_issues,
completed: module.completed_issues,
cancelled: module.cancelled_issues,
backlog: moduleDetails.backlog_issues,
unstarted: moduleDetails.unstarted_issues,
started: moduleDetails.started_issues,
completed: moduleDetails.completed_issues,
cancelled: moduleDetails.cancelled_issues,
}}
totalIssues={module.total_issues}
module={module}
totalIssues={moduleDetails.total_issues}
module={moduleDetails}
/>
</div>
</>
@ -544,9 +566,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</button>
</div>
<div className="mt-2 space-y-2 hover:bg-custom-background-80">
{memberRole && module.link_module && module.link_module.length > 0 ? (
{memberRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (
<LinksList
links={module.link_module}
links={moduleDetails.link_module}
handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink}
userAuth={memberRole}
@ -571,4 +593,4 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div>
</>
);
};
});

View File

@ -230,15 +230,16 @@ export const ProfileIssuesView = () => {
mutateProfileIssues();
}}
/>
{issueToDelete && (
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
onSubmit={async () => {
mutateProfileIssues();
}}
/>
)}
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">

View File

@ -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))";

View File

@ -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,85 +43,34 @@ 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 (
<IssueViewContextProvider>
<AppLayout header={<CycleIssuesHeader />} withProjectWrapper>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
handleOnSubmit={async () => {}}
/>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
link={
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}>
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<p className="truncate">{`${truncateText(
cycleDetails?.project_detail.name ?? "Project",
32
)} Cycles`}</p>
</a>
</Link>
}
/>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
{cycleDetails?.name && truncateText(cycleDetails.name, 40)}
</>
}
className="ml-1.5 flex-shrink-0"
width="auto"
>
{cycles?.map((cycle) => (
<CustomMenu.MenuItem
key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
>
{truncateText(cycle.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
right={
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
<CycleIssuesHeader />
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
cycleSidebar ? "rotate-180" : ""
}`}
onClick={() => setCycleSidebar((prevData) => !prevData)}
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
}
>
{error ? (
<EmptyState
image={emptyCycle}
@ -160,27 +84,18 @@ const SingleCycle: React.FC = () => {
) : (
<>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div
className={`relative w-full h-full flex flex-col overflow-auto ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
<div className="relative w-full h-full flex overflow-auto">
<div className={`flex flex-col h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<div className="h-full w-full">
<CycleLayoutRoot />
</div>
<CycleDetailsSidebar
cycleStatus={cycleStatus}
cycle={cycleDetails}
isOpen={cycleSidebar}
isCompleted={cycleStatus === "completed" ?? false}
user={user}
/>
</div>
{cycleId && <CycleDetailsSidebar isOpen={!isSidebarCollapsed} cycleId={cycleId.toString()} />}
</div>
</>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
</AppLayout>
);
};

View File

@ -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 (
<>
<AppLayout header={<ModuleIssuesHeader />} withProjectWrapper>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: true }}
handleOnSubmit={handleAddIssuesToModule}
handleOnSubmit={async () => {}}
/>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs onBack={() => router.back()}>
<BreadcrumbItem
link={
<Link href={`/${workspaceSlug}/projects/${projectId}/modules`}>
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<p className="truncate">{`${truncateText(
moduleDetails?.project_detail.name ?? "Project",
32
)} Modules`}</p>
</a>
</Link>
}
/>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
className="ml-1.5"
width="auto"
>
{modules?.map((module) => (
<CustomMenu.MenuItem
key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
>
{truncateText(module.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
right={<ModuleIssuesHeader />}
>
{error ? (
<EmptyState
image={emptyModule}
@ -140,23 +79,14 @@ const SingleModule: React.FC = () => {
}}
/>
) : (
<>
<div
className={`relative overflow-y-auto h-full flex flex-col ${
moduleSidebar ? "mr-[24rem]" : ""
} duration-300`}
>
<div className="flex h-full w-full">
<div className={`h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
<ModuleLayoutRoot />
</div>
<ModuleDetailsSidebar
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}
user={user}
/>
</>
{moduleId && <ModuleDetailsSidebar isOpen={!isSidebarCollapsed} moduleId={moduleId.toString()} />}
</div>
)}
</ProjectAuthorizationWrapper>
</AppLayout>
</>
);
};

View File

@ -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<any>;
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;
}
};
}

View File

@ -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
);
}
};

View File

@ -33,6 +33,7 @@ export interface IIssueStore {
// action
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
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;

View File

@ -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<IIssue>;
// creating issue
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => Promise<IIssue>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
// updating issue
updateIssue: (
workspaceId: string,
projectId: string,
issueId: string,
data: Partial<IIssue>,
user: IUser | undefined
) => void;
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => void;
// deleting issue
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void;
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
@ -210,13 +204,15 @@ export class IssueDetailStore implements IIssueDetailStore {
}
};
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => {
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
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<IIssue>,
user: IUser | undefined
) => {
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => {
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);

View File

@ -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,

View File

@ -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
);
}
};

View File

@ -34,6 +34,9 @@ export interface IModuleIssueStore {
// action
fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>;
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<any>;
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise<any>;
}
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;
}
};
}

View File

@ -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
);
}
};

View File

@ -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<IModule>;
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => 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;
}
};

View File

@ -36,6 +36,7 @@ export interface IProfileIssueStore {
// action
fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise<any>;
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,

View File

@ -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(() => {