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