mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
9bddd2eb67
commit
d78b4dccf3
@ -323,6 +323,10 @@ module.exports = {
|
|||||||
"0%": { right: "-20rem" },
|
"0%": { right: "-20rem" },
|
||||||
"100%": { right: "0" },
|
"100%": { right: "0" },
|
||||||
},
|
},
|
||||||
|
"bar-loader": {
|
||||||
|
from: { left: "-100%" },
|
||||||
|
to: { left: "100%" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
typography: ({ theme }) => ({
|
typography: ({ theme }) => ({
|
||||||
brand: {
|
brand: {
|
||||||
|
@ -35,7 +35,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
React.useState<HTMLDivElement | null>(null);
|
React.useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-start",
|
placement: placement ?? "auto",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className={`relative w-min text-left ${className}`}>
|
<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
|
<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"
|
maxHeight === "lg"
|
||||||
? "max-h-60"
|
? "max-h-60"
|
||||||
: maxHeight === "md"
|
: maxHeight === "md"
|
||||||
|
@ -223,7 +223,6 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
handleClose={() => toggleDeleteIssueModal(false)}
|
handleClose={() => toggleDeleteIssueModal(false)}
|
||||||
isOpen={isDeleteIssueModalOpen}
|
isOpen={isDeleteIssueModalOpen}
|
||||||
data={issueDetails}
|
data={issueDetails}
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// headless ui
|
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// hooks
|
||||||
@ -28,32 +30,39 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
|
||||||
import { isDateGreaterThanToday, renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import {
|
||||||
|
getDateRangeStatus,
|
||||||
|
isDateGreaterThanToday,
|
||||||
|
renderDateFormat,
|
||||||
|
renderShortDateWithYearFormat,
|
||||||
|
} from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IUser, ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cycle: ICycle | undefined;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
cycleStatus: string;
|
cycleId: string;
|
||||||
isCompleted: boolean;
|
|
||||||
user: IUser | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
const cycleService = new CycleService();
|
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 [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
const { workspaceSlug, projectId } = router.query;
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
|
||||||
cycleId: string;
|
|
||||||
};
|
const user = userStore.currentUser ?? undefined;
|
||||||
|
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -78,9 +87,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`)
|
||||||
|
|
||||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -96,11 +103,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cycle)
|
if (cycleDetails)
|
||||||
reset({
|
reset({
|
||||||
...cycle,
|
...cycleDetails,
|
||||||
});
|
});
|
||||||
}, [cycle, reset]);
|
}, [cycleDetails, reset]);
|
||||||
|
|
||||||
const dateChecker = async (payload: any) => {
|
const dateChecker = async (payload: any) => {
|
||||||
try {
|
try {
|
||||||
@ -129,11 +136,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cycle?.start_date && cycle?.end_date) {
|
if (cycleDetails?.start_date && cycleDetails?.end_date) {
|
||||||
const isDateValidForExistingCycle = await dateChecker({
|
const isDateValidForExistingCycle = await dateChecker({
|
||||||
start_date: `${watch("start_date")}`,
|
start_date: `${watch("start_date")}`,
|
||||||
end_date: `${watch("end_date")}`,
|
end_date: `${watch("end_date")}`,
|
||||||
cycle_id: cycle.id,
|
cycle_id: cycleDetails.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
if (isDateValidForExistingCycle) {
|
||||||
@ -203,11 +210,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cycle?.start_date && cycle?.end_date) {
|
if (cycleDetails?.start_date && cycleDetails?.end_date) {
|
||||||
const isDateValidForExistingCycle = await dateChecker({
|
const isDateValidForExistingCycle = await dateChecker({
|
||||||
start_date: `${watch("start_date")}`,
|
start_date: `${watch("start_date")}`,
|
||||||
end_date: `${watch("end_date")}`,
|
end_date: `${watch("end_date")}`,
|
||||||
cycle_id: cycle.id,
|
cycle_id: cycleDetails.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDateValidForExistingCycle) {
|
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 cycleStatus =
|
||||||
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{cycle && (
|
{cycleDetails && workspaceSlug && projectId && (
|
||||||
<CycleDeleteModal
|
<CycleDeleteModal
|
||||||
cycle={cycle}
|
cycle={cycleDetails}
|
||||||
modal={cycleDeleteModal}
|
modal={cycleDeleteModal}
|
||||||
modalClose={() => setCycleDeleteModal(false)}
|
modalClose={() => setCycleDeleteModal(false)}
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId}
|
projectId={projectId.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`fixed top-[66px] z-20 ${
|
className={`absolute top-0 z-20 ${
|
||||||
isOpen ? "right-0" : "-right-[24rem]"
|
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 flex-col items-start justify-center">
|
||||||
<div className="flex gap-2.5 px-5 text-sm">
|
<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
|
<Popover.Button
|
||||||
disabled={isCompleted ?? false}
|
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 ${
|
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" />
|
<CalendarDays className="h-3 w-3" />
|
||||||
<span>
|
<span>
|
||||||
{renderShortDateWithYearFormat(
|
{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"
|
"Start date"
|
||||||
)}
|
)}
|
||||||
</span>
|
</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">
|
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||||
<CustomRangeDatePicker
|
<CustomRangeDatePicker
|
||||||
value={watch("start_date") ? watch("start_date") : cycle?.start_date}
|
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
handleStartDateChange(val);
|
handleStartDateChange(val);
|
||||||
@ -344,14 +361,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
<Popover.Button
|
<Popover.Button
|
||||||
disabled={isCompleted ?? false}
|
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 ${
|
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" />
|
<CalendarDays className="h-3 w-3" />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{renderShortDateWithYearFormat(
|
{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"
|
"End date"
|
||||||
)}
|
)}
|
||||||
</span>
|
</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">
|
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||||
<CustomRangeDatePicker
|
<CustomRangeDatePicker
|
||||||
value={watch("end_date") ? watch("end_date") : cycle?.end_date}
|
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
handleEndDateChange(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 flex-col items-start justify-start gap-2">
|
||||||
<div className="flex w-full items-start justify-between gap-2">
|
<div className="flex w-full items-start justify-between gap-2">
|
||||||
<div className="max-w-[300px]">
|
<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>
|
</div>
|
||||||
<CustomMenu width="lg" ellipsis>
|
<CustomMenu width="lg" ellipsis>
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
@ -412,7 +431,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
||||||
{cycle.description}
|
{cycleDetails.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -424,20 +443,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5">
|
<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
|
<img
|
||||||
src={cycle.owned_by.avatar}
|
src={cycleDetails.owned_by.avatar}
|
||||||
height={12}
|
height={12}
|
||||||
width={12}
|
width={12}
|
||||||
className="rounded-full"
|
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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<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>
|
||||||
</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">
|
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||||
<span className="h-4 w-4">
|
<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>
|
</span>
|
||||||
{cycle.completed_issues}/{cycle.total_issues}
|
{cycleDetails.completed_issues}/{cycleDetails.total_issues}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -498,7 +517,8 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Pending Issues -{" "}
|
Pending Issues -{" "}
|
||||||
{cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}
|
{cycleDetails.total_issues -
|
||||||
|
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -515,10 +535,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
distribution={cycle.distribution.completion_chart}
|
distribution={cycleDetails.distribution.completion_chart}
|
||||||
startDate={cycle.start_date ?? ""}
|
startDate={cycleDetails.start_date ?? ""}
|
||||||
endDate={cycle.end_date ?? ""}
|
endDate={cycleDetails.end_date ?? ""}
|
||||||
totalIssues={cycle.total_issues}
|
totalIssues={cycleDetails.total_issues}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<span className="font-medium text-custom-text-200">Other Information</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cycle.total_issues > 0 ? (
|
{cycleDetails.total_issues > 0 ? (
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -555,18 +575,18 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{cycle.total_issues > 0 ? (
|
{cycleDetails.total_issues > 0 ? (
|
||||||
<div className="h-full w-full py-4">
|
<div className="h-full w-full py-4">
|
||||||
<SidebarProgressStats
|
<SidebarProgressStats
|
||||||
distribution={cycle.distribution}
|
distribution={cycleDetails.distribution}
|
||||||
groupedIssues={{
|
groupedIssues={{
|
||||||
backlog: cycle.backlog_issues,
|
backlog: cycleDetails.backlog_issues,
|
||||||
unstarted: cycle.unstarted_issues,
|
unstarted: cycleDetails.unstarted_issues,
|
||||||
started: cycle.started_issues,
|
started: cycleDetails.started_issues,
|
||||||
completed: cycle.completed_issues,
|
completed: cycleDetails.completed_issues,
|
||||||
cancelled: cycle.cancelled_issues,
|
cancelled: cycleDetails.cancelled_issues,
|
||||||
}}
|
}}
|
||||||
totalIssues={cycle.total_issues}
|
totalIssues={cycleDetails.total_issues}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -595,4 +615,4 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -19,7 +19,9 @@ type Props = {
|
|||||||
|
|
||||||
const cycleService = new CycleService();
|
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 router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
@ -33,8 +35,9 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
|
|||||||
const transferableIssuesCount = cycleDetails
|
const transferableIssuesCount = cycleDetails
|
||||||
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
|
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
<span>Completed cycles are not editable.</span>
|
<span>Completed cycles are not editable.</span>
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Plus } from "lucide-react";
|
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -28,9 +32,15 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
cycleIssueFilter: cycleIssueFilterStore,
|
cycleIssueFilter: cycleIssueFilterStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
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(
|
const handleLayoutChange = useCallback(
|
||||||
(layout: TIssueLayouts) => {
|
(layout: TIssueLayouts) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -88,6 +98,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
[issueFilterStore, projectId, workspaceSlug]
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cyclesList = projectId ? cycleStore.cycles[projectId.toString()] : undefined;
|
||||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,6 +108,39 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
cycleDetails={cycleDetails ?? undefined}
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutSelection
|
<LayoutSelection
|
||||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||||
@ -141,6 +185,14 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Issue
|
Add Issue
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
import { ProjectAnalyticsModal } from "components/analytics";
|
import { ProjectAnalyticsModal } from "components/analytics";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Plus } from "lucide-react";
|
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -28,9 +32,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
moduleFilter: moduleFilterStore,
|
moduleFilter: moduleFilterStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
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(
|
const handleLayoutChange = useCallback(
|
||||||
(layout: TIssueLayouts) => {
|
(layout: TIssueLayouts) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -88,6 +98,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
[issueFilterStore, projectId, workspaceSlug]
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,6 +108,42 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
onClose={() => setAnalyticsModal(false)}
|
onClose={() => setAnalyticsModal(false)}
|
||||||
moduleDetails={moduleDetails ?? undefined}
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutSelection
|
<LayoutSelection
|
||||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||||
@ -141,6 +188,14 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Issue
|
Add Issue
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,56 +1,31 @@
|
|||||||
import { useEffect, useState, Fragment } from "react";
|
import { useEffect, useState, Fragment } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { mutate } from "swr";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
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";
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IIssue, IUser, ISubIssueResponse } from "types";
|
import type { IIssue } 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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data: IIssue | null;
|
data: IIssue;
|
||||||
user: IUser | undefined;
|
|
||||||
onSubmit?: () => Promise<void>;
|
onSubmit?: () => Promise<void>;
|
||||||
redirection?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueService = new IssueService();
|
export const DeleteIssueModal: React.FC<Props> = observer((props) => {
|
||||||
const issueArchiveService = new IssueArchiveService();
|
const { data, isOpen, handleClose, onSubmit } = props;
|
||||||
|
|
||||||
export const DeleteIssueModal: React.FC<Props> = ({
|
|
||||||
isOpen,
|
|
||||||
handleClose,
|
|
||||||
data,
|
|
||||||
user,
|
|
||||||
onSubmit,
|
|
||||||
redirection = true,
|
|
||||||
}) => {
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, issueId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
|
||||||
|
|
||||||
const { displayFilters, params } = useIssuesView();
|
const { issueDetail: issueDetailStore } = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
@ -61,76 +36,15 @@ export const DeleteIssueModal: React.FC<Props> = ({
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleIssueDelete = async () => {
|
||||||
if (!workspaceSlug || !data) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await issueService
|
await issueDetailStore.deleteIssue(workspaceSlug.toString(), data.project, data.id);
|
||||||
.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);
|
|
||||||
|
|
||||||
return {
|
if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false));
|
||||||
...prevData,
|
|
||||||
sub_issues: updatedArray,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
|
||||||
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
|
||||||
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSubmit) onSubmit();
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
setToastAlert({
|
|
||||||
title: "Success",
|
|
||||||
type: "success",
|
|
||||||
message: "Issue deleted successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (issueId && redirection) router.back();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
});
|
|
||||||
if (onSubmit) await onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
@ -194,4 +108,4 @@ export const DeleteIssueModal: React.FC<Props> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -9,14 +9,17 @@ import { Spinner } from "@plane/ui";
|
|||||||
// types
|
// types
|
||||||
import { ICalendarWeek } from "./types";
|
import { ICalendarWeek } from "./types";
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssueGroupedStructure | null;
|
issues: IIssueGroupedStructure | null;
|
||||||
layout: "month" | "week" | undefined;
|
layout: "month" | "week" | undefined;
|
||||||
|
showWeekends: boolean;
|
||||||
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarChart: React.FC<Props> = observer((props) => {
|
export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||||
const { issues, layout } = props;
|
const { issues, layout, showWeekends, quickActions } = props;
|
||||||
|
|
||||||
const { calendar: calendarStore } = useMobxStore();
|
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">
|
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||||
<CalendarHeader />
|
<CalendarHeader />
|
||||||
<CalendarWeekHeader />
|
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
|
||||||
<div className="h-full w-full overflow-y-auto">
|
<div className="h-full w-full overflow-y-auto">
|
||||||
{layout === "month" ? (
|
{layout === "month" ? (
|
||||||
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
||||||
{allWeeksOfActiveMonth &&
|
{allWeeksOfActiveMonth &&
|
||||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||||
<CalendarWeekDays key={weekIndex} week={week} issues={issues} />
|
<CalendarWeekDays key={weekIndex} week={week} issues={issues} quickActions={quickActions} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} />
|
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} quickActions={quickActions} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,11 +11,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
|||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
// constants
|
// constants
|
||||||
import { MONTHS_LIST } from "constants/calendar";
|
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) => {
|
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||||
const { date, issues } = props;
|
const { date, issues, quickActions } = props;
|
||||||
|
|
||||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
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() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
||||||
{date.date.getDate()}
|
{date.date.getDate()}
|
||||||
</div>
|
</div>
|
||||||
<CalendarIssueBlocks issues={issuesList} />
|
<CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// icons
|
// icons
|
||||||
@ -14,6 +14,21 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
|
|||||||
|
|
||||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
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 { activeMonthDate } = calendarStore.calendarFilters;
|
||||||
|
|
||||||
const getWeekLayoutHeader = (): string => {
|
const getWeekLayoutHeader = (): string => {
|
||||||
@ -47,10 +62,17 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<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"
|
{calendarLayout === "month"
|
||||||
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
|
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
|
||||||
: getWeekLayoutHeader()}
|
: getWeekLayoutHeader()}
|
||||||
|
</button>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -61,8 +83,13 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel>
|
<Popover.Panel className="fixed z-50">
|
||||||
<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">
|
<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">
|
<div className="flex items-center justify-between gap-2 pb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
@ -20,6 +20,21 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
|
|||||||
|
|
||||||
const { issueFilter: issueFilterStore, calendar: calendarStore } = useMobxStore();
|
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 calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||||
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
||||||
|
|
||||||
@ -57,12 +72,12 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => {
|
{({ open }) => (
|
||||||
if (open) {
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<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 ${
|
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"
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
@ -73,6 +88,7 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
<ChevronUp width={12} strokeWidth={2} />
|
<ChevronUp width={12} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -83,8 +99,13 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel>
|
<Popover.Panel className="fixed z-50">
|
||||||
<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">
|
<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>
|
<div>
|
||||||
{Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => (
|
{Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => (
|
||||||
<button
|
<button
|
||||||
@ -110,8 +131,7 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
|
|||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "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) => {
|
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||||
const { issues } = props;
|
const { issues, quickActions } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -21,7 +23,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
|
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
|
||||||
<a
|
<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
|
snapshot.isDragging
|
||||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||||
: "bg-custom-background-100 hover: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}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
|
<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>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { CalendarChart } from "components/issues";
|
import { CalendarChart, CycleIssueQuickActions } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const CycleCalendarLayout: React.FC = observer(() => {
|
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
|
// TODO: add drag and drop functionality
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@ -26,12 +31,43 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const issues = cycleIssueStore.getIssues;
|
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 (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<CalendarChart
|
<CalendarChart
|
||||||
issues={issues as IIssueGroupedStructure | null}
|
issues={issues as IIssueGroupedStructure | null}
|
||||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
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>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { CalendarChart } from "components/issues";
|
import { CalendarChart, ModuleIssueQuickActions } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const ModuleCalendarLayout: React.FC = observer(() => {
|
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
|
// TODO: add drag and drop functionality
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@ -26,12 +35,45 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const issues = moduleIssueStore.getIssues;
|
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 (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<CalendarChart
|
<CalendarChart
|
||||||
issues={issues as IIssueGroupedStructure | null}
|
issues={issues as IIssueGroupedStructure | null}
|
||||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
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>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { CalendarChart } from "components/issues";
|
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const CalendarLayout: React.FC = observer(() => {
|
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
|
// TODO: add drag and drop functionality
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@ -26,12 +31,35 @@ export const CalendarLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const issues = issueStore.getIssues;
|
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 (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<CalendarChart
|
<CalendarChart
|
||||||
issues={issues as IIssueGroupedStructure | null}
|
issues={issues as IIssueGroupedStructure | null}
|
||||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
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>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { CalendarChart } from "components/issues";
|
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
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
|
// TODO: add drag and drop functionality
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@ -26,12 +35,35 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const issues = projectViewIssuesStore.getIssues;
|
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 (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<CalendarChart
|
<CalendarChart
|
||||||
issues={issues as IIssueGroupedStructure | null}
|
issues={issues as IIssueGroupedStructure | null}
|
||||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
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>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,14 +9,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
|
|||||||
// types
|
// types
|
||||||
import { ICalendarDate, ICalendarWeek } from "./types";
|
import { ICalendarDate, ICalendarWeek } from "./types";
|
||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssueGroupedStructure | null;
|
issues: IIssueGroupedStructure | null;
|
||||||
week: ICalendarWeek | undefined;
|
week: ICalendarWeek | undefined;
|
||||||
|
quickActions: (issue: IIssue) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||||
const { issues, week } = props;
|
const { issues, week, quickActions } = props;
|
||||||
|
|
||||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
@ -34,7 +36,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
|||||||
{Object.values(week).map((date: ICalendarDate) => {
|
{Object.values(week).map((date: ICalendarDate) => {
|
||||||
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// constants
|
// constants
|
||||||
import { DAYS_LIST } from "constants/calendar";
|
import { DAYS_LIST } from "constants/calendar";
|
||||||
|
|
||||||
export const CalendarWeekHeader: React.FC = observer(() => {
|
type Props = {
|
||||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
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 (
|
return (
|
||||||
<div
|
<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"
|
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) => {
|
{Object.values(DAYS_LIST).map((day) => {
|
||||||
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
||||||
|
|
||||||
|
@ -111,8 +111,8 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
|||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<FilterExtraOptions
|
<FilterExtraOptions
|
||||||
selectedExtraOptions={{
|
selectedExtraOptions={{
|
||||||
show_empty_groups: displayFilters.show_empty_groups ?? false,
|
show_empty_groups: displayFilters.show_empty_groups ?? true,
|
||||||
sub_issue: displayFilters.sub_issue ?? false,
|
sub_issue: displayFilters.sub_issue ?? true,
|
||||||
}}
|
}}
|
||||||
handleUpdate={(key, val) =>
|
handleUpdate={(key, val) =>
|
||||||
handleDisplayFiltersUpdate({
|
handleDisplayFiltersUpdate({
|
||||||
|
@ -20,6 +20,8 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const activeGroupBy = selectedGroupBy ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
@ -35,7 +37,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={groupBy?.key}
|
key={groupBy?.key}
|
||||||
isChecked={selectedGroupBy === groupBy?.key ? true : false}
|
isChecked={activeGroupBy === groupBy?.key ? true : false}
|
||||||
onClick={() => handleUpdate(groupBy.key)}
|
onClick={() => handleUpdate(groupBy.key)}
|
||||||
title={groupBy.title}
|
title={groupBy.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
|
@ -18,6 +18,8 @@ export const FilterIssueType: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
|
const activeIssueType = selectedIssueType ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
@ -30,7 +32,7 @@ export const FilterIssueType: React.FC<Props> = observer((props) => {
|
|||||||
{ISSUE_FILTER_OPTIONS.map((issueType) => (
|
{ISSUE_FILTER_OPTIONS.map((issueType) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={issueType?.key}
|
key={issueType?.key}
|
||||||
isChecked={selectedIssueType === issueType?.key ? true : false}
|
isChecked={activeIssueType === issueType?.key ? true : false}
|
||||||
onClick={() => handleUpdate(issueType?.key)}
|
onClick={() => handleUpdate(issueType?.key)}
|
||||||
title={issueType.title}
|
title={issueType.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
|
@ -19,6 +19,8 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const activeOrderBy = selectedOrderBy ?? "-created_at";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterHeader
|
<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) => (
|
{ISSUE_ORDER_BY_OPTIONS.filter((option) => orderByOptions.includes(option.key)).map((orderBy) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={orderBy?.key}
|
key={orderBy?.key}
|
||||||
isChecked={selectedOrderBy === orderBy?.key ? true : false}
|
isChecked={activeOrderBy === orderBy?.key ? true : false}
|
||||||
onClick={() => handleUpdate(orderBy.key)}
|
onClick={() => handleUpdate(orderBy.key)}
|
||||||
title={orderBy.title}
|
title={orderBy.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// filters
|
// filters
|
||||||
export * from "./filters";
|
export * from "./filters";
|
||||||
|
export * from "./quick-action-dropdowns";
|
||||||
|
|
||||||
// layouts
|
// layouts
|
||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
@ -1,50 +1,58 @@
|
|||||||
// react beautiful dnd
|
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
// components
|
// components
|
||||||
import { KanBanProperties } from "./properties";
|
import { KanBanProperties } from "./properties";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
columnId: string;
|
columnId: string;
|
||||||
issues: any;
|
index: number;
|
||||||
|
issue: IIssue;
|
||||||
isDragDisabled: boolean;
|
isDragDisabled: boolean;
|
||||||
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
|
handleIssues: (
|
||||||
display_properties: any;
|
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 = ({
|
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||||
sub_group_id,
|
const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
|
||||||
columnId,
|
|
||||||
issues,
|
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||||
isDragDisabled,
|
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
|
||||||
handleIssues,
|
};
|
||||||
display_properties,
|
|
||||||
}: IssueBlockProps) => (
|
return (
|
||||||
<>
|
<>
|
||||||
{issues && issues.length > 0 ? (
|
<Draggable draggableId={issue.id} index={index} isDragDisabled={isDragDisabled}>
|
||||||
<>
|
{(provided, snapshot) => (
|
||||||
{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) => (
|
|
||||||
<div
|
<div
|
||||||
key={issue.id}
|
className="group/kanban-block relative p-1.5 hover:cursor-default"
|
||||||
className="p-1.5 hover:cursor-default"
|
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
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
|
<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 ${
|
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`
|
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{display_properties && display_properties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
|
<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 className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||||
<div>
|
<div>
|
||||||
@ -52,22 +60,14 @@ export const IssueBlock = ({
|
|||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
columnId={columnId}
|
columnId={columnId}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
handleIssues={handleIssues}
|
handleIssues={updateIssue}
|
||||||
display_properties={display_properties}
|
display_properties={displayProperties}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</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>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
50
web/components/issues/issue-layouts/kanban/blocks-list.tsx
Normal file
50
web/components/issues/issue-layouts/kanban/blocks-list.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,14 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
// react beautiful dnd
|
import { useRouter } from "next/router";
|
||||||
import { DragDropContext } from "@hello-pangea/dnd";
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { DragDropContext } from "@hello-pangea/dnd";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// store
|
import { CycleIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
@ -20,7 +21,11 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
cycleIssueKanBanView: cycleIssueKanBanViewStore,
|
cycleIssueKanBanView: cycleIssueKanBanViewStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, cycleId } = router.query;
|
||||||
|
|
||||||
const issues = cycleIssueStore?.getIssues;
|
const issues = cycleIssueStore?.getIssues;
|
||||||
|
|
||||||
@ -50,9 +55,27 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
: cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
: cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => {
|
const handleIssues = useCallback(
|
||||||
cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
(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) => {
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||||
@ -74,7 +97,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -91,7 +122,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// react beautiful dnd
|
import { observer } from "mobx-react-lite";
|
||||||
import { Droppable } from "@hello-pangea/dnd";
|
import { Droppable } from "@hello-pangea/dnd";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||||
import { IssueBlock } from "./block";
|
import { KanbanIssueBlocksList } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
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 {
|
export interface IGroupByKanBan {
|
||||||
issues: any;
|
issues: any;
|
||||||
@ -20,14 +19,20 @@ export interface IGroupByKanBan {
|
|||||||
list: any;
|
list: any;
|
||||||
listKey: string;
|
listKey: string;
|
||||||
isDragDisabled: boolean;
|
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;
|
display_properties: any;
|
||||||
kanBanToggle: any;
|
kanBanToggle: any;
|
||||||
handleKanBanToggle: any;
|
handleKanBanToggle: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
({
|
const {
|
||||||
issues,
|
issues,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
group_by,
|
group_by,
|
||||||
@ -36,10 +41,12 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
|||||||
listKey,
|
listKey,
|
||||||
isDragDisabled,
|
isDragDisabled,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
kanBanToggle,
|
kanBanToggle,
|
||||||
handleKanBanToggle,
|
handleKanBanToggle,
|
||||||
}) => {
|
} = props;
|
||||||
|
|
||||||
const verticalAlignPosition = (_list: any) =>
|
const verticalAlignPosition = (_list: any) =>
|
||||||
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
|
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
|
||||||
|
|
||||||
@ -78,12 +85,13 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{issues ? (
|
{issues ? (
|
||||||
<IssueBlock
|
<KanbanIssueBlocksList
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
columnId={getValueFromObject(_list, listKey) as string}
|
columnId={getValueFromObject(_list, listKey) as string}
|
||||||
issues={issues[getValueFromObject(_list, listKey) as string]}
|
issues={issues[getValueFromObject(_list, listKey) as string]}
|
||||||
isDragDisabled={isDragDisabled}
|
isDragDisabled={isDragDisabled}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -102,8 +110,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export interface IKanBan {
|
export interface IKanBan {
|
||||||
issues: any;
|
issues: any;
|
||||||
@ -111,7 +118,13 @@ export interface IKanBan {
|
|||||||
group_by: string | null;
|
group_by: string | null;
|
||||||
sub_group_id?: string;
|
sub_group_id?: string;
|
||||||
handleDragDrop?: (result: any) => void | undefined;
|
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;
|
display_properties: any;
|
||||||
kanBanToggle: any;
|
kanBanToggle: any;
|
||||||
handleKanBanToggle: any;
|
handleKanBanToggle: any;
|
||||||
@ -132,6 +145,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
group_by,
|
group_by,
|
||||||
sub_group_id = "null",
|
sub_group_id = "null",
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
kanBanToggle,
|
kanBanToggle,
|
||||||
handleKanBanToggle,
|
handleKanBanToggle,
|
||||||
@ -144,7 +158,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
estimates,
|
estimates,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
@ -158,6 +172,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -174,6 +189,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -190,6 +206,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -206,6 +223,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -222,6 +240,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -238,6 +257,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from "./block";
|
||||||
|
export * from "./blocks-list";
|
||||||
export * from "./cycle-root";
|
export * from "./cycle-root";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
// react beautiful dnd
|
import { useRouter } from "next/router";
|
||||||
import { DragDropContext } from "@hello-pangea/dnd";
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { DragDropContext } from "@hello-pangea/dnd";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// store
|
import { ModuleIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
@ -20,7 +21,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
moduleIssueKanBanView: moduleIssueKanBanViewStore,
|
moduleIssueKanBanView: moduleIssueKanBanViewStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, moduleId } = router.query;
|
||||||
|
|
||||||
const issues = moduleIssueStore?.getIssues;
|
const issues = moduleIssueStore?.getIssues;
|
||||||
|
|
||||||
@ -50,9 +55,27 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
: moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
: 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);
|
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) => {
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||||
@ -74,7 +97,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -91,7 +122,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { FC } from "react";
|
import { FC, useCallback } from "react";
|
||||||
import { DragDropContext } from "@hello-pangea/dnd";
|
import { useRouter } from "next/router";
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { DragDropContext } from "@hello-pangea/dnd";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// store
|
import { ProjectIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export interface IProfileIssuesKanBanLayout {}
|
export interface IProfileIssuesKanBanLayout {}
|
||||||
|
|
||||||
@ -20,7 +22,11 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
|||||||
profileIssues: profileIssuesStore,
|
profileIssues: profileIssuesStore,
|
||||||
profileIssueFilters: profileIssueFiltersStore,
|
profileIssueFilters: profileIssueFiltersStore,
|
||||||
issueKanBanView: issueKanBanViewStore,
|
issueKanBanView: issueKanBanViewStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const issues = profileIssuesStore?.getIssues;
|
const issues = profileIssuesStore?.getIssues;
|
||||||
|
|
||||||
@ -50,9 +56,18 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
|||||||
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
: 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);
|
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) => {
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||||
@ -74,7 +89,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -91,7 +113,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -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 { DragDropContext } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// store
|
import { ProjectIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
export interface IKanBanLayout {}
|
export interface IKanBanLayout {}
|
||||||
|
|
||||||
export const KanBanLayout: FC = observer(() => {
|
export const KanBanLayout: FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
issue: issueStore,
|
issue: issueStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
issueKanBanView: issueKanBanViewStore,
|
issueKanBanView: issueKanBanViewStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const issues = issueStore?.getIssues;
|
const issues = issueStore?.getIssues;
|
||||||
|
|
||||||
@ -48,9 +55,18 @@ export const KanBanLayout: FC = observer(() => {
|
|||||||
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
: 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);
|
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) => {
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||||
@ -72,7 +88,14 @@ export const KanBanLayout: FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -89,7 +112,14 @@ export const KanBanLayout: FC = observer(() => {
|
|||||||
issues={issues}
|
issues={issues}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={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}
|
display_properties={display_properties}
|
||||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||||
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
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 {
|
interface ISubGroupSwimlaneHeader {
|
||||||
issues: any;
|
issues: any;
|
||||||
@ -61,7 +61,13 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
|||||||
|
|
||||||
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||||
issues: any;
|
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;
|
display_properties: any;
|
||||||
kanBanToggle: any;
|
kanBanToggle: any;
|
||||||
handleKanBanToggle: any;
|
handleKanBanToggle: any;
|
||||||
@ -81,6 +87,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
list,
|
list,
|
||||||
listKey,
|
listKey,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
kanBanToggle,
|
kanBanToggle,
|
||||||
handleKanBanToggle,
|
handleKanBanToggle,
|
||||||
@ -130,6 +137,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_id={getValueFromObject(_list, listKey) as string}
|
sub_group_id={getValueFromObject(_list, listKey) as string}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -153,7 +161,13 @@ export interface IKanBanSwimLanes {
|
|||||||
issues: any;
|
issues: any;
|
||||||
sub_group_by: string | null;
|
sub_group_by: string | null;
|
||||||
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;
|
display_properties: any;
|
||||||
kanBanToggle: any;
|
kanBanToggle: any;
|
||||||
handleKanBanToggle: any;
|
handleKanBanToggle: any;
|
||||||
@ -172,6 +186,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
sub_group_by,
|
sub_group_by,
|
||||||
group_by,
|
group_by,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
kanBanToggle,
|
kanBanToggle,
|
||||||
handleKanBanToggle,
|
handleKanBanToggle,
|
||||||
@ -184,7 +199,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
estimates,
|
estimates,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { project: projectStore }: RootStore = useMobxStore();
|
const { project: projectStore } = useMobxStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -270,6 +285,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={projectStore?.projectStates}
|
list={projectStore?.projectStates}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -291,6 +307,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={ISSUE_STATE_GROUPS}
|
list={ISSUE_STATE_GROUPS}
|
||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -312,6 +329,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={ISSUE_PRIORITIES}
|
list={ISSUE_PRIORITIES}
|
||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -333,6 +351,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={projectStore?.projectLabels}
|
list={projectStore?.projectLabels}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -354,6 +373,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={projectStore?.projectMembers}
|
list={projectStore?.projectMembers}
|
||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
@ -375,6 +395,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
list={projectStore?.projectMembers}
|
list={projectStore?.projectMembers}
|
||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
kanBanToggle={kanBanToggle}
|
kanBanToggle={kanBanToggle}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
@ -62,45 +62,47 @@ export const ViewKanBanLayout: React.FC = observer(() => {
|
|||||||
const projects = projectStore?.projectStates || null;
|
const projects = projectStore?.projectStates || null;
|
||||||
const estimates = null;
|
const estimates = null;
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
// return (
|
||||||
{currentKanBanView === "default" ? (
|
// <div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||||
<KanBan
|
// <DragDropContext onDragEnd={onDragEnd}>
|
||||||
issues={issues}
|
// {currentKanBanView === "default" ? (
|
||||||
sub_group_by={sub_group_by}
|
// <KanBan
|
||||||
group_by={group_by}
|
// issues={issues}
|
||||||
handleIssues={updateIssue}
|
// sub_group_by={sub_group_by}
|
||||||
display_properties={display_properties}
|
// group_by={group_by}
|
||||||
kanBanToggle={() => {}}
|
// handleIssues={updateIssue}
|
||||||
handleKanBanToggle={() => {}}
|
// display_properties={display_properties}
|
||||||
states={states}
|
// kanBanToggle={() => {}}
|
||||||
stateGroups={stateGroups}
|
// handleKanBanToggle={() => {}}
|
||||||
priorities={priorities}
|
// states={states}
|
||||||
labels={labels}
|
// stateGroups={stateGroups}
|
||||||
members={members}
|
// priorities={priorities}
|
||||||
projects={projects}
|
// labels={labels}
|
||||||
estimates={estimates}
|
// members={members}
|
||||||
/>
|
// projects={projects}
|
||||||
) : (
|
// estimates={estimates}
|
||||||
<KanBanSwimLanes
|
// />
|
||||||
issues={issues}
|
// ) : (
|
||||||
sub_group_by={sub_group_by}
|
// <KanBanSwimLanes
|
||||||
group_by={group_by}
|
// issues={issues}
|
||||||
handleIssues={updateIssue}
|
// sub_group_by={sub_group_by}
|
||||||
display_properties={display_properties}
|
// group_by={group_by}
|
||||||
kanBanToggle={() => {}}
|
// handleIssues={updateIssue}
|
||||||
handleKanBanToggle={() => {}}
|
// display_properties={display_properties}
|
||||||
states={states}
|
// kanBanToggle={() => {}}
|
||||||
stateGroups={stateGroups}
|
// handleKanBanToggle={() => {}}
|
||||||
priorities={priorities}
|
// states={states}
|
||||||
labels={labels}
|
// stateGroups={stateGroups}
|
||||||
members={members}
|
// priorities={priorities}
|
||||||
projects={projects}
|
// labels={labels}
|
||||||
estimates={estimates}
|
// members={members}
|
||||||
/>
|
// projects={projects}
|
||||||
)}
|
// estimates={estimates}
|
||||||
</DragDropContext>
|
// />
|
||||||
</div>
|
// )}
|
||||||
);
|
// </DragDropContext>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { FC } from "react";
|
|
||||||
// components
|
// components
|
||||||
import { KanBanProperties } from "./properties";
|
import { KanBanProperties } from "./properties";
|
||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
columnId: string;
|
columnId: string;
|
||||||
issues: any;
|
issue: IIssue;
|
||||||
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;
|
display_properties: any;
|
||||||
states: any;
|
states: any;
|
||||||
labels: any;
|
labels: any;
|
||||||
@ -16,53 +18,48 @@ interface IssueBlockProps {
|
|||||||
priorities: any;
|
priorities: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlock: FC<IssueBlockProps> = (props) => {
|
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||||
const { columnId, issues, handleIssues, display_properties, states, labels, members, priorities } = props;
|
const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, priorities } =
|
||||||
|
props;
|
||||||
|
|
||||||
const handleIssue = (_issue: any) => {
|
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
||||||
if (_issue && handleIssues) handleIssues(!columnId && columnId === "null" ? null : columnId, _issue);
|
if (issueToUpdate && handleIssues) handleIssues(group_by, issueToUpdate, "update");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issues &&
|
<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">
|
||||||
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`}
|
|
||||||
>
|
|
||||||
{display_properties && display_properties?.key && (
|
{display_properties && display_properties?.key && (
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
<div className="flex-shrink-0 text-xs text-custom-text-300">
|
||||||
{issue?.project_detail?.identifier}-{issue.sequence_id}
|
{issue?.project_detail?.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
workspaceSlug={issue?.workspace_detail?.slug}
|
workspaceSlug={issue?.workspace_detail?.slug}
|
||||||
projectId={issue?.project_detail?.id}
|
projectId={issue?.project_detail?.id}
|
||||||
issueId={issue?.id}
|
issueId={issue?.id}
|
||||||
handleIssue={handleIssue}
|
// TODO: add the logic here
|
||||||
|
handleIssue={() => {}}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<div className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full">{issue.name}</div>
|
<div className="line-clamp-1 text-sm font-medium text-custom-text-100 w-full">{issue.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IssuePeekOverview>
|
</IssuePeekOverview>
|
||||||
|
|
||||||
<div className="ml-auto flex-shrink-0">
|
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
|
||||||
<KanBanProperties
|
<KanBanProperties
|
||||||
columnId={columnId}
|
columnId={columnId}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
handleIssues={handleIssues}
|
handleIssues={updateIssue}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
members={members}
|
members={members}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
/>
|
/>
|
||||||
|
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
43
web/components/issues/issue-layouts/list/blocks-list.tsx
Normal file
43
web/components/issues/issue-layouts/list/blocks-list.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,21 +1,28 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { List } from "./default";
|
import { List } from "./default";
|
||||||
// store
|
import { CycleIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
export interface ICycleListLayout {}
|
export interface ICycleListLayout {}
|
||||||
|
|
||||||
export const CycleListLayout: React.FC = observer(() => {
|
export const CycleListLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, cycleId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const issues = cycleIssueStore?.getIssues;
|
const issues = cycleIssueStore?.getIssues;
|
||||||
|
|
||||||
@ -23,9 +30,27 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const display_properties = issueFilterStore?.userDisplayProperties || null;
|
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);
|
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 states = projectStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
@ -40,7 +65,15 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
<List
|
<List
|
||||||
issues={issues}
|
issues={issues}
|
||||||
group_by={group_by}
|
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}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
|
@ -5,13 +5,16 @@ import { ListGroupByHeaderRoot } from "./headers/group-by-root";
|
|||||||
import { IssueBlock } from "./block";
|
import { IssueBlock } from "./block";
|
||||||
// constants
|
// constants
|
||||||
import { getValueFromObject } from "constants/issue";
|
import { getValueFromObject } from "constants/issue";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
import { IssueBlocksList } from "./blocks-list";
|
||||||
|
|
||||||
export interface IGroupByList {
|
export interface IGroupByList {
|
||||||
issues: any;
|
issues: any;
|
||||||
group_by: string | null;
|
group_by: string | null;
|
||||||
list: any;
|
list: any;
|
||||||
listKey: string;
|
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;
|
display_properties: any;
|
||||||
is_list?: boolean;
|
is_list?: boolean;
|
||||||
states: any;
|
states: any;
|
||||||
@ -30,6 +33,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
list,
|
list,
|
||||||
listKey,
|
listKey,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
is_list = false,
|
is_list = false,
|
||||||
states,
|
states,
|
||||||
@ -59,10 +63,11 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`w-full h-full relative transition-all`}>
|
<div className={`w-full h-full relative transition-all`}>
|
||||||
{issues && (
|
{issues && (
|
||||||
<IssueBlock
|
<IssueBlocksList
|
||||||
columnId={getValueFromObject(_list, listKey) as string}
|
columnId={getValueFromObject(_list, listKey) as string}
|
||||||
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
|
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -77,11 +82,13 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: update all the types
|
||||||
export interface IList {
|
export interface IList {
|
||||||
issues: any;
|
issues: any;
|
||||||
group_by: string | null;
|
group_by: string | null;
|
||||||
handleDragDrop?: (result: any) => void | undefined;
|
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;
|
display_properties: any;
|
||||||
states: any;
|
states: any;
|
||||||
labels: any;
|
labels: any;
|
||||||
@ -97,6 +104,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
issues,
|
issues,
|
||||||
group_by,
|
group_by,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
display_properties,
|
display_properties,
|
||||||
states,
|
states,
|
||||||
labels,
|
labels,
|
||||||
@ -116,6 +124,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={[{ id: "null", title: "All Issues" }]}
|
list={[{ id: "null", title: "All Issues" }]}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
is_list={true}
|
is_list={true}
|
||||||
states={states}
|
states={states}
|
||||||
@ -135,6 +144,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={projects}
|
list={projects}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -153,6 +163,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={states}
|
list={states}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -171,6 +182,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={stateGroups}
|
list={stateGroups}
|
||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -189,6 +201,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={priorities}
|
list={priorities}
|
||||||
listKey={`key`}
|
listKey={`key`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -207,6 +220,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={labels}
|
list={labels}
|
||||||
listKey={`id`}
|
listKey={`id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -225,6 +239,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={members}
|
list={members}
|
||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
@ -243,6 +258,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||||||
list={members}
|
list={members}
|
||||||
listKey={`member.id`}
|
listKey={`member.id`}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
display_properties={display_properties}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from "./block";
|
||||||
|
export * from "./blocks-list";
|
||||||
export * from "./cycle-root";
|
export * from "./cycle-root";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { List } from "./default";
|
import { List } from "./default";
|
||||||
// store
|
import { ModuleIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
export interface IModuleListLayout {}
|
export interface IModuleListLayout {}
|
||||||
|
|
||||||
export const ModuleListLayout: React.FC = observer(() => {
|
export const ModuleListLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, moduleId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
}: RootStore = useMobxStore();
|
issueDetail: issueDetailStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const issues = moduleIssueStore?.getIssues;
|
const issues = moduleIssueStore?.getIssues;
|
||||||
|
|
||||||
@ -23,9 +30,27 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const display_properties = issueFilterStore?.userDisplayProperties || null;
|
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);
|
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 states = projectStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
@ -40,7 +65,15 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
<List
|
<List
|
||||||
issues={issues}
|
issues={issues}
|
||||||
group_by={group_by}
|
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}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { List } from "./default";
|
import { List } from "./default";
|
||||||
// store
|
import { ProjectIssueQuickActions } from "components/issues";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
@ -15,18 +18,31 @@ export const ProfileIssuesListLayout: FC = observer(() => {
|
|||||||
workspace: workspaceStore,
|
workspace: workspaceStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
profileIssueFilters: profileIssueFiltersStore,
|
profileIssueFilters: profileIssueFiltersStore,
|
||||||
profileIssues: profileIssuesIssueStore,
|
profileIssues: profileIssuesStore,
|
||||||
}: RootStore = useMobxStore();
|
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 group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
|
||||||
|
|
||||||
const display_properties = profileIssueFiltersStore?.userDisplayProperties || null;
|
const display_properties = profileIssueFiltersStore?.userDisplayProperties || null;
|
||||||
|
|
||||||
const updateIssue = (group_by: string | null, issue: any) => {
|
const handleIssues = useCallback(
|
||||||
profileIssuesIssueStore.updateIssueStructure(group_by, null, issue);
|
(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 states = projectStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
@ -41,7 +57,14 @@ export const ProfileIssuesListLayout: FC = observer(() => {
|
|||||||
<List
|
<List
|
||||||
issues={issues}
|
issues={issues}
|
||||||
group_by={group_by}
|
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}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
|
@ -10,11 +10,13 @@ import { IssuePropertyEstimates } from "../properties/estimates";
|
|||||||
import { IssuePropertyDate } from "../properties/date";
|
import { IssuePropertyDate } from "../properties/date";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export interface IKanBanProperties {
|
export interface IKanBanProperties {
|
||||||
columnId: string;
|
columnId: string;
|
||||||
issue: any;
|
issue: any;
|
||||||
handleIssues?: (group_by: string | null, issue: any) => void;
|
handleIssues?: (group_by: string | null, issue: IIssue) => void;
|
||||||
display_properties: any;
|
display_properties: any;
|
||||||
states: any;
|
states: any;
|
||||||
labels: any;
|
labels: any;
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
import { FC } from "react";
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
|
||||||
import { List } from "./default";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { List } from "./default";
|
||||||
|
import { ProjectIssueQuickActions } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "store/root";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
export const ListLayout: FC = observer(() => {
|
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;
|
const issues = issueStore?.getIssues;
|
||||||
|
|
||||||
@ -18,9 +28,18 @@ export const ListLayout: FC = observer(() => {
|
|||||||
|
|
||||||
const display_properties = issueFilterStore?.userDisplayProperties || null;
|
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);
|
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 states = projectStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
@ -31,11 +50,18 @@ export const ListLayout: FC = observer(() => {
|
|||||||
const estimates = null;
|
const estimates = null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full h-full bg-custom-background-90`}>
|
<div className="relative w-full h-full bg-custom-background-90">
|
||||||
<List
|
<List
|
||||||
issues={issues}
|
issues={issues}
|
||||||
group_by={group_by}
|
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}
|
display_properties={display_properties}
|
||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
|
@ -31,21 +31,23 @@ export const ViewListLayout: React.FC = observer(() => {
|
|||||||
const projects = projectStore?.projectStates || null;
|
const projects = projectStore?.projectStates || null;
|
||||||
const estimates = null;
|
const estimates = null;
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<div className={`relative w-full h-full bg-custom-background-90`}>
|
|
||||||
<List
|
// return (
|
||||||
issues={issues}
|
// <div className={`relative w-full h-full bg-custom-background-90`}>
|
||||||
group_by={group_by}
|
// <List
|
||||||
handleIssues={updateIssue}
|
// issues={issues}
|
||||||
display_properties={display_properties}
|
// group_by={group_by}
|
||||||
states={states}
|
// handleIssues={updateIssue}
|
||||||
stateGroups={stateGroups}
|
// display_properties={display_properties}
|
||||||
priorities={priorities}
|
// states={states}
|
||||||
labels={labels}
|
// stateGroups={stateGroups}
|
||||||
members={members}
|
// priorities={priorities}
|
||||||
projects={projects}
|
// labels={labels}
|
||||||
estimates={estimates}
|
// members={members}
|
||||||
/>
|
// projects={projects}
|
||||||
</div>
|
// estimates={estimates}
|
||||||
);
|
// />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./cycle-issue";
|
||||||
|
export * from "./module-issue";
|
||||||
|
export * from "./project-issue";
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Popover2 } from "@blueprintjs/popover2";
|
import { Popover2 } from "@blueprintjs/popover2";
|
||||||
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
|
import { MoreHorizontal, Pencil, Trash2, ChevronRight, Link } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// helpers
|
// helpers
|
||||||
@ -82,6 +82,20 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||||
content={
|
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">
|
<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
|
<button
|
||||||
type="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="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
|
<button
|
||||||
type="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={() => {
|
onClick={() => {
|
||||||
handleDeleteIssue(issue);
|
handleDeleteIssue(issue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@ -109,20 +123,6 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
|
@ -45,7 +45,7 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<IssueColumn
|
<IssueColumn
|
||||||
issue={issue}
|
issue={issue}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -62,7 +62,7 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
!isLoading &&
|
!isLoading &&
|
||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue) => (
|
||||||
<SpreadsheetIssuesColumn
|
<SpreadsheetIssuesColumn
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
@ -75,6 +75,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,10 +20,8 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
user: userStore,
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
|
||||||
const issues = cycleIssueStore.getIssues;
|
const issues = cycleIssueStore.getIssues;
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
@ -41,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleUpdateIssue = useCallback(
|
const handleUpdateIssue = useCallback(
|
||||||
(issue: IIssue, data: Partial<IIssue>) => {
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !user) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...issue,
|
...issue,
|
||||||
@ -49,9 +47,9 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cycleIssueStore.updateIssueStructure(null, null, payload);
|
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 (
|
return (
|
||||||
|
@ -20,10 +20,8 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
user: userStore,
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
|
||||||
const issues = moduleIssueStore.getIssues;
|
const issues = moduleIssueStore.getIssues;
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
@ -41,7 +39,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleUpdateIssue = useCallback(
|
const handleUpdateIssue = useCallback(
|
||||||
(issue: IIssue, data: Partial<IIssue>) => {
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !user) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...issue,
|
...issue,
|
||||||
@ -49,9 +47,9 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
moduleIssueStore.updateIssueStructure(null, null, payload);
|
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 (
|
return (
|
||||||
|
@ -49,7 +49,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
issueStore.updateIssueStructure(null, null, payload);
|
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]
|
[issueStore, issueDetailStore, projectId, user, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
@ -20,10 +20,8 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
projectViewIssues: projectViewIssueStore,
|
projectViewIssues: projectViewIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
user: userStore,
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
|
||||||
const issues = projectViewIssueStore.getIssues;
|
const issues = projectViewIssueStore.getIssues;
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
@ -41,7 +39,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleUpdateIssue = useCallback(
|
const handleUpdateIssue = useCallback(
|
||||||
(issue: IIssue, data: Partial<IIssue>) => {
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !user) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...issue,
|
...issue,
|
||||||
@ -49,9 +47,9 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
projectViewIssueStore.updateIssueStructure(null, null, payload);
|
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 (
|
return (
|
||||||
|
@ -30,7 +30,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const issueUpdate = (_data: Partial<IIssue>) => {
|
const issueUpdate = (_data: Partial<IIssue>) => {
|
||||||
if (handleIssue) {
|
if (handleIssue) {
|
||||||
handleIssue(_data);
|
handleIssue(_data);
|
||||||
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data, undefined);
|
issueDetailStore.updateIssue(workspaceSlug, projectId, issueId, _data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,13 +16,12 @@ import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
|||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
// fetch-keys
|
// 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 {
|
export interface IssuesModalProps {
|
||||||
data?: IIssue | null;
|
data?: IIssue | null;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isUpdatingSingleIssue?: boolean;
|
|
||||||
prePopulateData?: Partial<IIssue>;
|
prePopulateData?: Partial<IIssue>;
|
||||||
fieldsToShow?: (
|
fieldsToShow?: (
|
||||||
| "project"
|
| "project"
|
||||||
@ -46,15 +45,7 @@ const issueService = new IssueService();
|
|||||||
const issueDraftService = new IssueDraftService();
|
const issueDraftService = new IssueDraftService();
|
||||||
|
|
||||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||||
const {
|
const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
|
||||||
data,
|
|
||||||
handleClose,
|
|
||||||
isOpen,
|
|
||||||
isUpdatingSingleIssue = false,
|
|
||||||
prePopulateData: prePopulateDataProps,
|
|
||||||
fieldsToShow = ["all"],
|
|
||||||
onSubmit,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
@ -211,10 +202,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createIssue = async (payload: Partial<IIssue>) => {
|
const createIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !activeProject || !user) return;
|
if (!workspaceSlug || !activeProject) return;
|
||||||
|
|
||||||
await issueDetailStore
|
await issueDetailStore
|
||||||
.createIssue(workspaceSlug.toString(), activeProject, payload, user)
|
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
@ -280,16 +271,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
|
|
||||||
await issueService
|
await issueService
|
||||||
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
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);
|
|
||||||
|
|
||||||
if (!createMore) onFormSubmitClose();
|
if (!createMore) onFormSubmitClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -6,7 +6,6 @@ import { IssueLabelService } from "services/issue";
|
|||||||
// hooks
|
// hooks
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
|
||||||
// components
|
// components
|
||||||
import { FiltersList } from "components/core";
|
import { FiltersList } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
@ -43,8 +42,6 @@ export const MyIssuesView: React.FC<Props> = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
|
||||||
|
|
||||||
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
||||||
|
|
||||||
@ -220,15 +217,16 @@ export const MyIssuesView: React.FC<Props> = () => {
|
|||||||
mutateMyIssues();
|
mutateMyIssues();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{issueToDelete && (
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
user={user}
|
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
mutateMyIssues();
|
mutateMyIssues();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{areFiltersApplied && (
|
{areFiltersApplied && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
|
@ -8,8 +8,6 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
// components
|
// components
|
||||||
import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues";
|
import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues";
|
||||||
// types
|
// types
|
||||||
@ -40,8 +38,6 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
|
|||||||
|
|
||||||
const issue = issues[peekIssue?.toString() ?? ""];
|
const issue = issues[peekIssue?.toString() ?? ""];
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
delete query.peekIssue;
|
delete query.peekIssue;
|
||||||
@ -53,17 +49,17 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
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));
|
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
|
||||||
if (handleMutation) handleMutation();
|
if (handleMutation) handleMutation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteIssue = async () => {
|
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();
|
if (handleMutation) handleMutation();
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -92,13 +88,14 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{issue && (
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
data={issue ? { ...issue } : null}
|
data={issue}
|
||||||
onSubmit={handleDeleteIssue}
|
onSubmit={handleDeleteIssue}
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
|
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||||
|
@ -277,12 +277,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
createIssueLink={handleCreateLink}
|
createIssueLink={handleCreateLink}
|
||||||
updateIssueLink={handleUpdateLink}
|
updateIssueLink={handleUpdateLink}
|
||||||
/>
|
/>
|
||||||
<DeleteIssueModal
|
{issueDetail && (
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
<DeleteIssueModal handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetail} />
|
||||||
isOpen={deleteIssueModal}
|
)}
|
||||||
data={issueDetail ?? null}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
<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">
|
<div className="flex items-center justify-between px-5 pb-3">
|
||||||
<h4 className="text-sm font-medium">
|
<h4 className="text-sm font-medium">
|
||||||
|
@ -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
|
<DeleteIssueModal
|
||||||
isOpen={issueCrudOperation?.delete?.toggle}
|
isOpen={issueCrudOperation?.delete?.toggle}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
@ -343,8 +346,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
|||||||
handleIssueCrudOperation("delete", null, null);
|
handleIssueCrudOperation("delete", null, null);
|
||||||
}}
|
}}
|
||||||
data={issueCrudOperation?.delete?.issue}
|
data={issueCrudOperation?.delete?.issue}
|
||||||
user={user}
|
|
||||||
redirection={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { ModuleService } from "services/module.service";
|
import { ModuleService } from "services/module.service";
|
||||||
// contexts
|
// contexts
|
||||||
@ -14,6 +17,7 @@ import useToast from "hooks/use-toast";
|
|||||||
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
|
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
|
||||||
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
|
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
|
||||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||||
|
// ui
|
||||||
import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui";
|
import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui";
|
||||||
// icon
|
// icon
|
||||||
import {
|
import {
|
||||||
@ -29,9 +33,9 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IUser, IIssue, linkDetails, IModule, ModuleLink } from "types";
|
import { linkDetails, IModule, ModuleLink } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||||
// constant
|
// constant
|
||||||
@ -46,21 +50,28 @@ const defaultValues: Partial<IModule> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
module?: IModule;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
moduleIssues?: IIssue[];
|
moduleId: string;
|
||||||
user: IUser | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
const moduleService = new ModuleService();
|
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 [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
||||||
const [moduleLinkModal, setModuleLinkModal] = useState(false);
|
const [moduleLinkModal, setModuleLinkModal] = useState(false);
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
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();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
@ -117,7 +128,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
|
|
||||||
const payload = { metadata: {}, ...formData };
|
const payload = { metadata: {}, ...formData };
|
||||||
|
|
||||||
const updatedLinks = module.link_module.map((l) =>
|
const updatedLinks = moduleDetails.link_module.map((l) =>
|
||||||
l.id === linkId
|
l.id === linkId
|
||||||
? {
|
? {
|
||||||
...l,
|
...l,
|
||||||
@ -146,7 +157,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
const handleDeleteLink = async (linkId: string) => {
|
const handleDeleteLink = async (linkId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !module) return;
|
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>(
|
mutate<IModule>(
|
||||||
MODULE_DETAILS(module.id),
|
MODULE_DETAILS(module.id),
|
||||||
@ -165,41 +176,45 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
// const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
|
||||||
|
|
||||||
copyTextToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Module link copied to clipboard",
|
title: "Link copied",
|
||||||
|
message: "Module link copied to clipboard",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Some error occurred",
|
title: "Error!",
|
||||||
|
message: "Some error occurred",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (module)
|
if (moduleDetails)
|
||||||
reset({
|
reset({
|
||||||
...module,
|
...moduleDetails,
|
||||||
members_list: module.members_list ?? module.members_detail?.map((m) => m.id),
|
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 isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date();
|
||||||
const isEndValid = new Date(`${module?.target_date}`) >= new Date(`${module?.start_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) => {
|
const handleEditLink = (link: linkDetails) => {
|
||||||
setSelectedLinkToUpdate(link);
|
setSelectedLinkToUpdate(link);
|
||||||
setModuleLinkModal(true);
|
setModuleLinkModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!moduleDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LinkModal
|
<LinkModal
|
||||||
@ -213,7 +228,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
createIssueLink={handleCreateLink}
|
createIssueLink={handleCreateLink}
|
||||||
updateIssueLink={handleUpdateLink}
|
updateIssueLink={handleUpdateLink}
|
||||||
/>
|
/>
|
||||||
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} user={user} />
|
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={moduleDetails} user={user} />
|
||||||
<div
|
<div
|
||||||
className={`fixed top-[66px] ${
|
className={`fixed top-[66px] ${
|
||||||
isOpen ? "right-0" : "-right-[24rem]"
|
isOpen ? "right-0" : "-right-[24rem]"
|
||||||
@ -254,11 +269,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<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 ${
|
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" />
|
<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>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -298,12 +315,14 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<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 ${
|
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 " />
|
<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>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<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 flex-col items-start justify-start gap-2">
|
||||||
<div className="flex w-full items-start justify-between gap-2 ">
|
<div className="flex w-full items-start justify-between gap-2 ">
|
||||||
<div className="max-w-[300px]">
|
<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>
|
</div>
|
||||||
<CustomMenu width="lg" ellipsis>
|
<CustomMenu width="lg" ellipsis>
|
||||||
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
|
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
|
||||||
@ -361,7 +382,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
||||||
{module.description}
|
{moduleDetails.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||||
<span className="h-4 w-4">
|
<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>
|
</span>
|
||||||
{module.completed_issues}/{module.total_issues}
|
{moduleDetails.completed_issues}/{moduleDetails.total_issues}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 w-full items-center justify-between gap-2 ">
|
||||||
<div className="flex items-center justify-start gap-2 text-sm">
|
<div className="flex items-center justify-start gap-2 text-sm">
|
||||||
<span className="font-medium text-custom-text-200">Progress</span>
|
<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]">
|
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
||||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||||
</span>
|
</span>
|
||||||
@ -439,7 +460,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{isStartValid && isEndValid && moduleIssues ? (
|
{isStartValid && isEndValid ? (
|
||||||
<div className=" h-full w-full py-4">
|
<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-start justify-between gap-4 py-2 text-xs">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -448,7 +469,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Pending Issues -{" "}
|
Pending Issues -{" "}
|
||||||
{module.total_issues - (module.completed_issues + module.cancelled_issues)}{" "}
|
{moduleDetails.total_issues -
|
||||||
|
(moduleDetails.completed_issues + moduleDetails.cancelled_issues)}{" "}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -465,10 +487,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative h-40 w-80">
|
<div className="relative h-40 w-80">
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
distribution={module.distribution.completion_chart}
|
distribution={moduleDetails.distribution.completion_chart}
|
||||||
startDate={module.start_date ?? ""}
|
startDate={moduleDetails.start_date ?? ""}
|
||||||
endDate={module.target_date ?? ""}
|
endDate={moduleDetails.target_date ?? ""}
|
||||||
totalIssues={module.total_issues}
|
totalIssues={moduleDetails.total_issues}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<span className="font-medium text-custom-text-200">Other Information</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{module.total_issues > 0 ? (
|
{moduleDetails.total_issues > 0 ? (
|
||||||
<Disclosure.Button className="p-1">
|
<Disclosure.Button className="p-1">
|
||||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -506,20 +528,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{module.total_issues > 0 ? (
|
{moduleDetails.total_issues > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className=" h-full w-full py-4">
|
<div className=" h-full w-full py-4">
|
||||||
<SidebarProgressStats
|
<SidebarProgressStats
|
||||||
distribution={module.distribution}
|
distribution={moduleDetails.distribution}
|
||||||
groupedIssues={{
|
groupedIssues={{
|
||||||
backlog: module.backlog_issues,
|
backlog: moduleDetails.backlog_issues,
|
||||||
unstarted: module.unstarted_issues,
|
unstarted: moduleDetails.unstarted_issues,
|
||||||
started: module.started_issues,
|
started: moduleDetails.started_issues,
|
||||||
completed: module.completed_issues,
|
completed: moduleDetails.completed_issues,
|
||||||
cancelled: module.cancelled_issues,
|
cancelled: moduleDetails.cancelled_issues,
|
||||||
}}
|
}}
|
||||||
totalIssues={module.total_issues}
|
totalIssues={moduleDetails.total_issues}
|
||||||
module={module}
|
module={moduleDetails}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -544,9 +566,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-2 hover:bg-custom-background-80">
|
<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
|
<LinksList
|
||||||
links={module.link_module}
|
links={moduleDetails.link_module}
|
||||||
handleEditLink={handleEditLink}
|
handleEditLink={handleEditLink}
|
||||||
handleDeleteLink={handleDeleteLink}
|
handleDeleteLink={handleDeleteLink}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
@ -571,4 +593,4 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -230,15 +230,16 @@ export const ProfileIssuesView = () => {
|
|||||||
mutateProfileIssues();
|
mutateProfileIssues();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{issueToDelete && (
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
user={user}
|
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
mutateProfileIssues();
|
mutateProfileIssues();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{areFiltersApplied && (
|
{areFiltersApplied && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
|
@ -56,6 +56,19 @@ export const copyTextToClipboard = async (text: string) => {
|
|||||||
await navigator.clipboard.writeText(text);
|
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 => {
|
export const generateRandomColor = (string: string): string => {
|
||||||
if (!string) return "rgb(var(--color-primary-100))";
|
if (!string) return "rgb(var(--color-primary-100))";
|
||||||
|
|
||||||
|
@ -1,65 +1,40 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// icons
|
// mobx store
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// contexts
|
|
||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
|
||||||
// components
|
// components
|
||||||
|
import { CycleIssuesHeader } from "components/headers";
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
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
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs, CustomMenu, ContrastIcon } from "@plane/ui";
|
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// images
|
// assets
|
||||||
import emptyCycle from "public/empty-state/cycle.svg";
|
import emptyCycle from "public/empty-state/cycle.svg";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
|
||||||
import { getDateRangeStatus } from "helpers/date-time.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 SingleCycle: React.FC = () => {
|
||||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
const [cycleSidebar, setCycleSidebar] = useState(true);
|
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
|
||||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { cycle: cycleStore } = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||||
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
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 { data: cycleDetails, error } = useSWR(
|
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
|
workspaceSlug && projectId && cycleId
|
||||||
? () => cycleService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -68,85 +43,34 @@ const SingleCycle: React.FC = () => {
|
|||||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
: "draft";
|
: "draft";
|
||||||
|
|
||||||
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
// TODO: add this function to bulk add issues to cycle
|
||||||
if (!workspaceSlug || !projectId) return;
|
// const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||||
|
// if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const payload = {
|
// const payload = {
|
||||||
issues: data.map((i) => i.id),
|
// issues: data.map((i) => i.id),
|
||||||
};
|
// };
|
||||||
|
|
||||||
await issueService
|
// await issueService
|
||||||
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
|
// .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
setToastAlert({
|
// setToastAlert({
|
||||||
type: "error",
|
// type: "error",
|
||||||
title: "Error!",
|
// title: "Error!",
|
||||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
// message: "Selected issues could not be added to the cycle. Please try again.",
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssueViewContextProvider>
|
<AppLayout header={<CycleIssuesHeader />} withProjectWrapper>
|
||||||
|
{/* TODO: Update logic to bulk add issues to a cycle */}
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={cycleIssuesListModal}
|
isOpen={cycleIssuesListModal}
|
||||||
handleClose={() => setCycleIssuesListModal(false)}
|
handleClose={() => setCycleIssuesListModal(false)}
|
||||||
searchParams={{ cycle: true }}
|
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 ? (
|
{error ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyCycle}
|
image={emptyCycle}
|
||||||
@ -160,27 +84,18 @@ const SingleCycle: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||||
|
<div className="relative w-full h-full flex overflow-auto">
|
||||||
<div
|
<div className={`flex flex-col h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
|
||||||
className={`relative w-full h-full flex flex-col overflow-auto ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
|
||||||
analyticsModal ? "mr-[50%]" : ""
|
|
||||||
} duration-300`}
|
|
||||||
>
|
|
||||||
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||||
|
<div className="h-full w-full">
|
||||||
<CycleLayoutRoot />
|
<CycleLayoutRoot />
|
||||||
</div>
|
</div>
|
||||||
<CycleDetailsSidebar
|
</div>
|
||||||
cycleStatus={cycleStatus}
|
{cycleId && <CycleDetailsSidebar isOpen={!isSidebarCollapsed} cycleId={cycleId.toString()} />}
|
||||||
cycle={cycleDetails}
|
</div>
|
||||||
isOpen={cycleSidebar}
|
|
||||||
isCompleted={cycleStatus === "completed" ?? false}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ProjectAuthorizationWrapper>
|
</AppLayout>
|
||||||
</IssueViewContextProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,134 +1,73 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// services
|
// mobx store
|
||||||
import { ModuleService } from "services/module.service";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { ModuleDetailsSidebar } from "components/modules";
|
import { ModuleDetailsSidebar } from "components/modules";
|
||||||
import { ModuleLayoutRoot } from "components/issues";
|
import { ModuleLayoutRoot } from "components/issues";
|
||||||
import { ModuleIssuesHeader } from "components/headers";
|
import { ModuleIssuesHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs, CustomMenu, DiceIcon } from "@plane/ui";
|
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// images
|
// assets
|
||||||
import emptyModule from "public/empty-state/module.svg";
|
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 SingleModule: React.FC = () => {
|
||||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||||
const [moduleSidebar, setModuleSidebar] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
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(
|
const { error } = useSWR(
|
||||||
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
|
workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : 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,
|
|
||||||
workspaceSlug && projectId && moduleId
|
workspaceSlug && projectId && moduleId
|
||||||
? () => moduleService.getModuleIssues(workspaceSlug as string, projectId as string, moduleId as string)
|
? () => moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: moduleDetails, error } = useSWR(
|
// TODO: add this function to bulk add issues to cycle
|
||||||
moduleId ? MODULE_DETAILS(moduleId as string) : null,
|
// const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
|
||||||
workspaceSlug && projectId
|
// if (!workspaceSlug || !projectId) return;
|
||||||
? () => moduleService.getModuleDetails(workspaceSlug as string, projectId as string, moduleId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
|
// const payload = {
|
||||||
if (!workspaceSlug || !projectId) return;
|
// issues: data.map((i) => i.id),
|
||||||
|
// };
|
||||||
|
|
||||||
const payload = {
|
// await moduleService
|
||||||
issues: data.map((i) => i.id),
|
// .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
|
// const openIssuesListModal = () => {
|
||||||
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
|
// setModuleIssuesListModal(true);
|
||||||
.catch(() =>
|
// };
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Selected issues could not be added to the module. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openIssuesListModal = () => {
|
|
||||||
setModuleIssuesListModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<AppLayout header={<ModuleIssuesHeader />} withProjectWrapper>
|
||||||
|
{/* TODO: Update logic to bulk add issues to a cycle */}
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={moduleIssuesListModal}
|
isOpen={moduleIssuesListModal}
|
||||||
handleClose={() => setModuleIssuesListModal(false)}
|
handleClose={() => setModuleIssuesListModal(false)}
|
||||||
searchParams={{ module: true }}
|
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 ? (
|
{error ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyModule}
|
image={emptyModule}
|
||||||
@ -140,23 +79,14 @@ const SingleModule: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex h-full w-full">
|
||||||
<div
|
<div className={`h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
|
||||||
className={`relative overflow-y-auto h-full flex flex-col ${
|
|
||||||
moduleSidebar ? "mr-[24rem]" : ""
|
|
||||||
} duration-300`}
|
|
||||||
>
|
|
||||||
<ModuleLayoutRoot />
|
<ModuleLayoutRoot />
|
||||||
</div>
|
</div>
|
||||||
<ModuleDetailsSidebar
|
{moduleId && <ModuleDetailsSidebar isOpen={!isSidebarCollapsed} moduleId={moduleId.toString()} />}
|
||||||
module={moduleDetails}
|
</div>
|
||||||
isOpen={moduleSidebar}
|
|
||||||
moduleIssues={moduleIssues}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</ProjectAuthorizationWrapper>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import { RootStore } from "../root";
|
|||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
// constants
|
// constants
|
||||||
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
||||||
|
|
||||||
@ -34,6 +35,9 @@ export interface ICycleIssueStore {
|
|||||||
// action
|
// action
|
||||||
fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
|
fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
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 {
|
export class CycleIssueStore implements ICycleIssueStore {
|
||||||
@ -52,9 +56,11 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
ungrouped: IIssue[];
|
ungrouped: IIssue[];
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
// service
|
|
||||||
cycleService;
|
// services
|
||||||
rootStore;
|
rootStore;
|
||||||
|
cycleService;
|
||||||
|
issueService;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -68,10 +74,14 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchIssues: action,
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteIssue: action,
|
||||||
|
addIssueToCycle: action,
|
||||||
|
removeIssueFromCycle: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
this.cycleService = new CycleService();
|
this.cycleService = new CycleService();
|
||||||
|
this.issueService = new IssueService();
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const workspaceSlug = this.rootStore.workspace.workspaceSlug;
|
const workspaceSlug = this.rootStore.workspace.workspaceSlug;
|
||||||
@ -130,7 +140,7 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
issues = issues as IIssueGroupedStructure;
|
issues = issues as IIssueGroupedStructure;
|
||||||
issues = {
|
issues = {
|
||||||
...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) {
|
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||||
@ -139,27 +149,55 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
...issues,
|
...issues,
|
||||||
[sub_group_id]: {
|
[sub_group_id]: {
|
||||||
...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") {
|
if (issueType === "ungrouped") {
|
||||||
issues = issues as IIssueUnGroupedStructure;
|
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 || "";
|
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
||||||
if (orderBy === "-created_at") {
|
if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "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") {
|
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||||
issues = sortArrayByDate(issues as any, "updated_at");
|
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") {
|
if (issueType === "ungrouped") {
|
||||||
issues = sortArrayByDate(issues as any, "updated_at");
|
issues = issues as IIssueUnGroupedStructure;
|
||||||
}
|
issues = issues.filter((i) => i?.id !== issue?.id);
|
||||||
if (orderBy === "priority") {
|
|
||||||
issues = sortArrayByPriority(issues as any, "priority");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -199,4 +237,44 @@ export class CycleIssueStore implements ICycleIssueStore {
|
|||||||
return error;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
|||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IIssueType } from "store/issue";
|
import { IIssueType } from "store/issue";
|
||||||
import { IUser } from "types";
|
|
||||||
|
|
||||||
export interface ICycleIssueKanBanViewStore {
|
export interface ICycleIssueKanBanViewStore {
|
||||||
kanBanToggle: {
|
kanBanToggle: {
|
||||||
@ -293,8 +292,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
{} as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -442,8 +440,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
{} as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -33,6 +33,7 @@ export interface IIssueStore {
|
|||||||
// action
|
// action
|
||||||
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
|
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
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 {
|
export class IssueStore implements IIssueStore {
|
||||||
@ -67,6 +68,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchIssues: action,
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteIssue: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
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) => {
|
fetchIssues = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
|
@ -3,7 +3,7 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"
|
|||||||
import { IssueService, IssueReactionService } from "services/issue";
|
import { IssueService, IssueReactionService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IUser, IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { groupReactionEmojis } from "constants/issue";
|
import { groupReactionEmojis } from "constants/issue";
|
||||||
|
|
||||||
@ -36,17 +36,11 @@ export interface IIssueDetailStore {
|
|||||||
// fetch issue details
|
// fetch issue details
|
||||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||||
// creating issue
|
// 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
|
// updating issue
|
||||||
updateIssue: (
|
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => void;
|
||||||
workspaceId: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
data: Partial<IIssue>,
|
|
||||||
user: IUser | undefined
|
|
||||||
) => void;
|
|
||||||
// deleting issue
|
// 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>;
|
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 {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = this.rootStore.user.currentUser ?? undefined;
|
||||||
|
|
||||||
const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user);
|
const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -237,13 +233,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateIssue = async (
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => {
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
data: Partial<IIssue>,
|
|
||||||
user: IUser | undefined
|
|
||||||
) => {
|
|
||||||
const newIssues = { ...this.issues };
|
const newIssues = { ...this.issues };
|
||||||
newIssues[issueId] = {
|
newIssues[issueId] = {
|
||||||
...newIssues[issueId],
|
...newIssues[issueId],
|
||||||
@ -257,6 +247,10 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
this.issues = newIssues;
|
this.issues = newIssues;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = this.rootStore.user.currentUser;
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user);
|
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user);
|
||||||
|
|
||||||
runInAction(() => {
|
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 };
|
const newIssues = { ...this.issues };
|
||||||
delete newIssues[issueId];
|
delete newIssues[issueId];
|
||||||
|
|
||||||
@ -293,12 +287,18 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
this.issues = newIssues;
|
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(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ export class IssueFilterStore implements IIssueFilterStore {
|
|||||||
labels: this.userFilters?.labels || undefined,
|
labels: this.userFilters?.labels || undefined,
|
||||||
start_date: this.userFilters?.start_date || undefined,
|
start_date: this.userFilters?.start_date || undefined,
|
||||||
target_date: this.userFilters?.target_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",
|
order_by: this.userDisplayFilters?.order_by || "-created_at",
|
||||||
sub_group_by: this.userDisplayFilters?.sub_group_by || undefined,
|
sub_group_by: this.userDisplayFilters?.sub_group_by || undefined,
|
||||||
type: this.userDisplayFilters?.type || undefined,
|
type: this.userDisplayFilters?.type || undefined,
|
||||||
|
@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
|||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IIssueType } from "./issue.store";
|
import { IIssueType } from "./issue.store";
|
||||||
import { IUser } from "types";
|
|
||||||
|
|
||||||
export interface IIssueKanBanViewStore {
|
export interface IIssueKanBanViewStore {
|
||||||
kanBanToggle: {
|
kanBanToggle: {
|
||||||
@ -293,8 +292,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
{} as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -442,8 +440,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
{} as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,9 @@ export interface IModuleIssueStore {
|
|||||||
// action
|
// action
|
||||||
fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>;
|
fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>;
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
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 {
|
export class ModuleIssueStore implements IModuleIssueStore {
|
||||||
@ -52,9 +55,10 @@ export class ModuleIssueStore implements IModuleIssueStore {
|
|||||||
ungrouped: IIssue[];
|
ungrouped: IIssue[];
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
// service
|
|
||||||
moduleService;
|
// services
|
||||||
rootStore;
|
rootStore;
|
||||||
|
moduleService;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -68,6 +72,9 @@ export class ModuleIssueStore implements IModuleIssueStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchIssues: action,
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteIssue: action,
|
||||||
|
addIssueToModule: action,
|
||||||
|
removeIssueFromModule: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
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) => {
|
fetchIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
@ -204,4 +247,44 @@ export class ModuleIssueStore implements IModuleIssueStore {
|
|||||||
return error;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
|||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IIssueType } from "../issue/issue.store";
|
import { IIssueType } from "../issue/issue.store";
|
||||||
import { IUser } from "types";
|
|
||||||
|
|
||||||
export interface IModuleIssueKanBanViewStore {
|
export interface IModuleIssueKanBanViewStore {
|
||||||
kanBanToggle: {
|
kanBanToggle: {
|
||||||
@ -293,8 +292,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
this.rootStore.user.currentUser as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -442,8 +440,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
|
|||||||
updateIssue.workspaceSlug,
|
updateIssue.workspaceSlug,
|
||||||
updateIssue.projectId,
|
updateIssue.projectId,
|
||||||
updateIssue.issueId,
|
updateIssue.issueId,
|
||||||
updateIssue,
|
updateIssue
|
||||||
this.rootStore.user.currentUser as IUser
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -38,7 +38,7 @@ export interface IModuleStore {
|
|||||||
getModuleById: (moduleId: string) => IModule | null;
|
getModuleById: (moduleId: string) => IModule | null;
|
||||||
|
|
||||||
fetchModules: (workspaceSlug: string, projectId: string) => void;
|
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>;
|
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
|
||||||
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => void;
|
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);
|
const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId);
|
||||||
|
|
||||||
if (!response) return null;
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.moduleDetails = {
|
this.moduleDetails = {
|
||||||
...this.moduleDetails,
|
...this.moduleDetails,
|
||||||
@ -181,6 +179,8 @@ export class ModuleStore implements IModuleStore {
|
|||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch module details in module store", 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.loader = false;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ export interface IProfileIssueStore {
|
|||||||
// action
|
// action
|
||||||
fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise<any>;
|
fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise<any>;
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
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 {
|
export class ProfileIssueStore implements IProfileIssueStore {
|
||||||
@ -76,6 +77,7 @@ export class ProfileIssueStore implements IProfileIssueStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchIssues: action,
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteIssue: action,
|
||||||
});
|
});
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
this.userService = new UserService();
|
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 (
|
fetchIssues = async (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -30,6 +30,7 @@ export interface IProjectViewIssuesStore {
|
|||||||
|
|
||||||
// actions
|
// actions
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
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: (
|
fetchViewIssues: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -72,6 +73,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
|
|||||||
|
|
||||||
// actions
|
// actions
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteIssue: action,
|
||||||
fetchViewIssues: action,
|
fetchViewIssues: action,
|
||||||
|
|
||||||
// computed
|
// 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) => {
|
fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user